diff --git a/README.md b/README.md index c1fe602..af7cbd6 100644 --- a/README.md +++ b/README.md @@ -65,29 +65,17 @@ Installed through a `sitecustomize.py` MetaPathFinder hook, so it runs at interp ### Auth issues -- **`Anthropic 401 authentication failed`** or **`No Anthropic credentials found`**: Hermes reads Claude subscription credentials from `~/.claude/.credentials.json`. If Claude Code is authenticated (e.g. in macOS Keychain) but that file is missing or stale, Hermes fails even when Claude Code itself works. Fix: +- **`Anthropic 401 authentication failed`** or **`No Anthropic credentials found`**: Hermes reads Claude subscription credentials from `~/.claude/.credentials.json`. If Claude Code is authenticated (e.g. in macOS Keychain) but that file is missing or stale, Hermes fails even when Claude Code itself works. + + On macOS, `install.sh` v1.1.1+ auto-mirrors the `Claude Code-credentials` Keychain entry into `~/.claude/.credentials.json` on every run, so re-running the installer is usually enough. Full fix: 1. Refresh Claude subscription login: ```bash claude auth login --claudeai ``` - 2. **macOS only** — mirror the Keychain `Claude Code-credentials` entry into the file Hermes reads: + 2. Re-run the installer to re-mirror credentials (macOS) and reload the patch: ```bash - python3 - <<'PY' - import subprocess - from pathlib import Path - - secret = subprocess.check_output( - ['security', 'find-generic-password', '-s', 'Claude Code-credentials', '-w'], - text=True, - ).strip() - - cred_path = Path.home() / '.claude' / '.credentials.json' - cred_path.parent.mkdir(parents=True, exist_ok=True) - cred_path.write_text(secret) - cred_path.chmod(0o600) - print(f'wrote {cred_path}') - PY + ./install.sh ``` 3. Remove stale `ANTHROPIC_TOKEN` / `ANTHROPIC_API_KEY` values from `~/.hermes/.env` — they can override subscription auth. 4. Reset cached credentials: @@ -99,7 +87,26 @@ Installed through a `sitecustomize.py` MetaPathFinder hook, so it runs at interp hermes chat -q 'Reply with exactly: AUTH TEST OK' --provider anthropic -m claude-sonnet-4-6 -Q ``` - Credit: this macOS Keychain mirror step was written up by [@DrQbz](https://github.com/DrQbz) in [issue #5](https://github.com/kristianvast/hermes-claude-auth/issues/5). + If the auto-mirror doesn't work (e.g. your Keychain entry is under a different service name), mirror it manually: + ```bash + python3 - <<'PY' + import subprocess + from pathlib import Path + + secret = subprocess.check_output( + ['security', 'find-generic-password', '-s', 'Claude Code-credentials', '-w'], + text=True, + ).strip() + + cred_path = Path.home() / '.claude' / '.credentials.json' + cred_path.parent.mkdir(parents=True, exist_ok=True) + cred_path.write_text(secret) + cred_path.chmod(0o600) + print(f'wrote {cred_path}') + PY + ``` + + Credit: the macOS Keychain mirror approach was written up by [@DrQbz](https://github.com/DrQbz) in [issue #5](https://github.com/kristianvast/hermes-claude-auth/issues/5) and is now automated in `install.sh`. ### Billing / routing issues diff --git a/anthropic_billing_bypass.py b/anthropic_billing_bypass.py index 345921c..86af169 100644 --- a/anthropic_billing_bypass.py +++ b/anthropic_billing_bypass.py @@ -87,11 +87,16 @@ Version history header, ``?beta=true`` query param on ``/v1/messages``. Addresses the "Third-party apps now draw from your extra usage, not your plan limits" 400 error introduced by Anthropic's 2026-04-14+ validator tightening. +- 1.1.1 (2026-04-22): Installer only — ``install.sh`` now auto-mirrors the + ``Claude Code-credentials`` macOS Keychain entry into + ``~/.claude/.credentials.json`` on Darwin hosts, so the oneliner works + end-to-end on macOS without a manual post-install step. Bypass module + itself is unchanged; version bump tracks the release. """ from __future__ import annotations -__version__ = "1.1.0" +__version__ = "1.1.1" import hashlib import inspect diff --git a/install.sh b/install.sh index c5b5934..4e592eb 100755 --- a/install.sh +++ b/install.sh @@ -61,6 +61,27 @@ fi chmod 644 "$SITECUSTOMIZE" printf "${GREEN}[✓] Installed hook into %s${RESET}\n" "$SITECUSTOMIZE" +# macOS: hermes-agent reads Claude subscription credentials from +# ~/.claude/.credentials.json, but Claude Code on macOS stores them in +# Keychain only. Mirror the Keychain entry into the file so auth works +# out of the box. No-op on Linux (Claude Code writes the file directly). +if [ "$(uname -s)" = "Darwin" ]; then + CRED_FILE="$HOME/.claude/.credentials.json" + if KEYCHAIN_CRED="$(security find-generic-password -s 'Claude Code-credentials' -w 2>/dev/null)"; then + mkdir -p "$(dirname "$CRED_FILE")" + if [ ! -f "$CRED_FILE" ] || [ "$(cat "$CRED_FILE" 2>/dev/null)" != "$KEYCHAIN_CRED" ]; then + printf '%s' "$KEYCHAIN_CRED" > "$CRED_FILE" + chmod 600 "$CRED_FILE" + printf "${GREEN}[✓] Mirrored Claude Code credentials from Keychain → %s${RESET}\n" "$CRED_FILE" + else + printf "${GREEN}[✓] Claude Code credentials file already matches Keychain${RESET}\n" + fi + elif [ ! -f "$CRED_FILE" ]; then + printf "${YELLOW}[!] macOS detected but no 'Claude Code-credentials' Keychain entry found${RESET}\n" + printf " Run: claude auth login --claudeai\n" + fi +fi + if systemctl --user is-active hermes-gateway.service >/dev/null 2>&1; then systemctl --user restart hermes-gateway.service printf "${GREEN}[✓] Restarted hermes-gateway.service${RESET}\n" diff --git a/tests/test_install.sh b/tests/test_install.sh index ad619e1..0e1d81b 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -139,6 +139,92 @@ else fail "$T5" "install.sh or uninstall.sh --purge exited non-zero" fi +# macOS Keychain mirror tests — fake `uname -s` → Darwin and a fake +# `security find-generic-password` via PATH shims so install.sh takes the +# Darwin branch without an actual Mac. +FAKE_BIN="$FAKE_HOME/fakebin" +mkdir -p "$FAKE_BIN" +cat > "$FAKE_BIN/uname" <<'UNAME_EOF' +#!/usr/bin/env bash +if [ "${1:-}" = "-s" ]; then + echo Darwin + exit 0 +fi +exec /usr/bin/uname "$@" +UNAME_EOF +chmod +x "$FAKE_BIN/uname" + +FAKE_CRED='{"oauth":{"accessToken":"sk-ant-fake","refreshToken":"rt-fake","expiresAt":0}}' +cat > "$FAKE_BIN/security" </dev/null 2>&1; then + ok=1 + CRED_FILE="$FAKE_HOME/.claude/.credentials.json" + assert_file_exists "$T6" "$CRED_FILE" || ok=0 + if [ -f "$CRED_FILE" ]; then + actual="$(cat "$CRED_FILE")" + if [ "$actual" != "$FAKE_CRED" ]; then + fail "$T6" "credentials content mismatch: got '$actual'" + ok=0 + fi + mode="$(python3 -c "import os, sys; print(oct(os.stat(sys.argv[1]).st_mode)[-3:])" "$CRED_FILE")" + if [ "$mode" != "600" ]; then + fail "$T6" "credentials file mode is $mode, expected 600" + ok=0 + fi + fi + [ "$ok" -eq 1 ] && pass "$T6" +else + fail "$T6" "install.sh exited non-zero under faked macOS" +fi + +# Test 7: Idempotent macOS re-install does not rewrite file with identical content +T7="Test 7: macOS re-install does not rewrite identical credentials" +CRED_FILE="$FAKE_HOME/.claude/.credentials.json" +if [ -f "$CRED_FILE" ]; then + mtime_before="$(python3 -c "import os, sys; print(os.stat(sys.argv[1]).st_mtime_ns)" "$CRED_FILE")" + sleep 1 + if PATH="$FAKE_BIN:$PATH" "$REPO_DIR/install.sh" >/dev/null 2>&1; then + mtime_after="$(python3 -c "import os, sys; print(os.stat(sys.argv[1]).st_mtime_ns)" "$CRED_FILE")" + if [ "$mtime_before" != "$mtime_after" ]; then + fail "$T7" "credentials file rewritten despite identical content" + else + pass "$T7" + fi + else + fail "$T7" "install.sh exited non-zero on idempotent macOS re-run" + fi +else + fail "$T7" "Test 6 did not produce a credentials file; cannot run idempotency check" +fi + +# Test 8: macOS install with no Keychain entry leaves credentials file absent +T8="Test 8: macOS install with missing Keychain entry leaves no file" +rm -rf "$FAKE_HOME/.claude" +cat > "$FAKE_BIN/security" <<'SECURITY_FAIL_EOF' +#!/usr/bin/env bash +exit 1 +SECURITY_FAIL_EOF +chmod +x "$FAKE_BIN/security" + +if PATH="$FAKE_BIN:$PATH" "$REPO_DIR/install.sh" >/dev/null 2>&1; then + assert_file_not_exists "$T8" "$FAKE_HOME/.claude/.credentials.json" && pass "$T8" +else + fail "$T8" "install.sh exited non-zero when Keychain entry absent" +fi + TOTAL=$((PASS + FAIL)) printf '\n%d/%d tests passed\n' "$PASS" "$TOTAL" [ "$FAIL" -eq 0 ]