Skip to content

fix(anthropic): honor CLAUDE_CONFIG_DIR when locating Claude Code credentials - #81252

Open
griffinwork40 wants to merge 1 commit into
NousResearch:mainfrom
griffinwork40:fix/anthropic-honor-claude-config-dir
Open

fix(anthropic): honor CLAUDE_CONFIG_DIR when locating Claude Code credentials#81252
griffinwork40 wants to merge 1 commit into
NousResearch:mainfrom
griffinwork40:fix/anthropic-honor-claude-config-dir

Conversation

@griffinwork40

Copy link
Copy Markdown

Symptom

A Linux/Windows user who sets CLAUDE_CONFIG_DIR to relocate Claude
Code's config directory gets silently switched from subscription/plan
billing to prepaid API credits
when using hermes-agent as an Anthropic
provider. agent/anthropic_adapter.py looked for Claude Code's OAuth
credentials at a hardcoded ~/.claude/.credentials.json, never checking
CLAUDE_CONFIG_DIR. When that file isn't found at the default path,
resolve_anthropic_token() falls through its priority chain all the way
to the ANTHROPIC_API_KEY branch — trading Bearer-auth OAuth (plan
allowance) for x-api-key auth (metered credits), with all of the
diagnostic breadcrumbs at logger.debug. The credential refresh
write-back
path had a second, worse instance of the same bug: on
refresh it wrote a stale duplicate credentials file to the default path
— a file Claude Code itself never reads.

Docs justification

Per Anthropic's official Claude Code docs
(https://code.claude.com/docs/en/authentication):

On Linux, credentials are stored in ~/.claude/.credentials.json. On
Windows, credentials are stored in
%USERPROFILE%\.claude\.credentials.json. If you've set the
CLAUDE_CONFIG_DIR environment variable on Linux or Windows, the
.credentials.json file lives under that directory instead.

CLAUDE_CONFIG_DIR was honored nowhere in this repo prior to this PR
(repo-wide grep returned 0 hits).

Fix

Added one module-level helper, _claude_code_credentials_path(), and
used it at both call sites that previously inlined the hardcoded path:

  • _read_claude_code_credentials_from_file()
  • _write_claude_code_credentials()

Behavior:

  • If CLAUDE_CONFIG_DIR is set and non-empty after .strip(), resolves
    to <that dir>/.credentials.json.
  • Empty/whitespace-only is treated as unset (matches the existing
    os.getenv(...).strip() convention already used in
    resolve_anthropic_token()).
  • Expands ~ and $VAR via os.path.expanduser + os.path.expandvars
    (covers CLAUDE_CONFIG_DIR=~/foo).
  • Resolved fresh on every call — never cached at import time — so
    processes that set/change the env var after import pick it up.

Platform-unconditional by design (no Darwin/Windows gate): when the
var is unset, the helper's output is byte-identical to the prior
hardcoded expression, so this is strictly additive. A platform check
would only add a branch with no behavioral benefit — macOS users who
never set CLAUDE_CONFIG_DIR get the same default path either way.

Relationship to #75146

Independent of, and does not conflict with, #75146 — that PR only
touches _read_claude_code_credentials_from_keychain (the macOS
Keychain path). This PR touches only the file read
(_read_claude_code_credentials_from_file) and the write-back
(_write_claude_code_credentials). Neither function nor line range
overlaps.

Testing

New file tests/agent/test_anthropic_claude_config_dir.py (14 tests,
real imports, no mocking of the module under test, tmp_path +
monkeypatch, invariant-based assertions):

  • read path honors CLAUDE_CONFIG_DIR
  • read path falls back to the default when unset
  • empty/whitespace-only value falls back to the default (parametrized)
  • ~ and $VAR expansion in the configured value
  • env var re-read at call time, not cached
  • write-back lands in the CLAUDE_CONFIG_DIR location, including a
    write → read round trip through the real read path
  • existing fields in the credentials file are preserved when writing
    under CLAUDE_CONFIG_DIR

Verification

export HERMES_PYTHON=<repo>/.venv/bin/python
scripts/run_tests.sh tests/agent/test_anthropic_claude_config_dir.py   # 16 passed, 0 failed
scripts/run_tests.sh tests/agent/test_anthropic_adapter.py             # 84 passed, 0 failed (no regressions)
ruff check .                                                            # All checks passed
$HERMES_PYTHON scripts/check-windows-footguns.py --all                 # 0 footguns (893 files scanned)

Confirmed the worktree's interpreter imports the worktree's own code
(not the main checkout) via python -c "import agent.anthropic_adapter as m; print(m.__file__)" before running any gate.

Scope

Minimal and surgical — one new helper, two call sites updated to use it,
no changes to the atomic-write / 0600 / os.replace logic in
_write_claude_code_credentials, no refactor of the surrounding file.

…dentials

agent/anthropic_adapter.py read and wrote Claude Code's OAuth credentials
at a hardcoded ~/.claude/.credentials.json, ignoring CLAUDE_CONFIG_DIR.
Per Anthropic's docs (https://code.claude.com/docs/en/authentication):
"If you've set the CLAUDE_CONFIG_DIR environment variable on Linux or
Windows, the .credentials.json file lives under that directory instead."

Impact: a Linux/Windows user who sets CLAUDE_CONFIG_DIR was silently not
found by hermes at the read site, which then fell through to the
ANTHROPIC_API_KEY branch of resolve_anthropic_token() — an unannounced
switch from subscription/plan billing (OAuth Bearer) to prepaid API
credits (x-api-key). The write-back site was worse: on refresh it wrote
a stale duplicate credentials file to the default path that Claude Code
itself never reads.

Add a single _claude_code_credentials_path() helper and use it at both
call sites (_read_claude_code_credentials_from_file,
_write_claude_code_credentials). It resolves CLAUDE_CONFIG_DIR at call
time (never cached), treats an empty/whitespace-only value as unset
(matching the os.getenv(...).strip() style already used in
resolve_anthropic_token()), and expands ~ / $VAR via os.path.expanduser
+ os.path.expandvars. Unconditional on platform: unset behaves
identically to before (strictly additive), and a Darwin/Windows gate
would only add a branch with no behavioral benefit.

Independent of and does not conflict with NousResearch#75146, which only touches
_read_claude_code_credentials_from_keychain (the macOS Keychain path);
this change touches only the file read and write-back path.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools labels Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants