Skip to content

fix: write apiKeyHelper to ~/.claude/settings.json for Claude Code v2.1.81+#1414

Open
lpcox wants to merge 4 commits intomainfrom
fix/claude-api-key-helper-settings-path
Open

fix: write apiKeyHelper to ~/.claude/settings.json for Claude Code v2.1.81+#1414
lpcox wants to merge 4 commits intomainfrom
fix/claude-api-key-helper-settings-path

Conversation

@lpcox
Copy link
Collaborator

@lpcox lpcox commented Mar 24, 2026

Problem

Claude Code v2.1.81+ reads apiKeyHelper from ~/.claude/settings.json, but the AWF entrypoint only writes it to ~/.claude.json (the legacy path). This causes Claude Code to report apiKeySource: "none" at init, and all API calls fail with EHOSTUNREACH.

Fixes: github/gh-aw#22713

Root Cause

The entrypoint writes the apiKeyHelper config (which tells Claude Code how to retrieve the API key placeholder for credential isolation) to $HOME/.claude.json. Newer versions of Claude Code (v2.1.81+) read this setting from $HOME/.claude/settings.json instead.

Fix

Write apiKeyHelper to both paths for backward/forward compatibility:

  • ~/.claude.json — legacy path (kept for older Claude Code versions)
  • ~/.claude/settings.json — new path (Claude Code v2.1.81+)

The ~/.claude directory is already bind-mounted read-write (docker-manager.ts:760), so settings.json is writable in both chroot and non-chroot modes.

Changes

  • containers/agent/entrypoint.sh — Refactored the apiKeyHelper config block into a reusable write_api_key_helper() function, called for both the legacy and new paths
  • src/docker-manager.ts — Updated comment to reflect dual-path behavior

Testing

  • All existing tests pass (3 pre-existing failures on main, unrelated to this change)
  • The ~/.claude bind mount already exists, so no docker-compose changes needed

….1.81+

Claude Code v2.1.81+ reads apiKeyHelper from ~/.claude/settings.json
instead of ~/.claude.json. The entrypoint now writes to both paths
for backward/forward compatibility.

Fixes: github/gh-aw#22713

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lpcox lpcox requested a review from Mossaka as a code owner March 24, 2026 18:40
Copilot AI review requested due to automatic review settings March 24, 2026 18:40
@github-actions
Copy link
Contributor

github-actions bot commented Mar 24, 2026

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.70% 82.80% 📈 +0.10%
Statements 82.37% 82.46% 📈 +0.09%
Functions 81.11% 81.11% ➡️ +0.00%
Branches 75.93% 75.94% ➡️ +0.01%
📁 Per-file Coverage Changes (1 files)
File Lines (Before → After) Statements (Before → After)
src/docker-manager.ts 86.3% → 86.7% (+0.42%) 85.8% → 86.1% (+0.38%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@lpcox
Copy link
Collaborator Author

lpcox commented Mar 24, 2026

@copilot please make sure that this fix does not compromise security. in other words, make sure that the actual api key is not shared with the agent.

Copy link
Contributor

Copilot AI commented Mar 24, 2026

@copilot please make sure that this fix does not compromise security. in other words, make sure that the actual api key is not shared with the agent.

The fix does not expose the real API key to the agent. Here's why:

What's written to the config files (~/.claude.json and ~/.claude/settings.json):

{"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"}

This is a path to a shell script, not the API key itself.

What that script returns (containers/agent/get-claude-key.sh):

sk-ant-placeholder-key-for-credential-isolation

A placeholder value — never the real credential.

Where the real key lives: Exclusively in the api-proxy sidecar container (172.30.0.30). When --enable-api-proxy is active, ANTHROPIC_API_KEY is explicitly added to EXCLUDED_ENV_VARS in docker-manager.ts:455, so the agent container never receives it.

How auth actually works: Claude Code invokes the helper, gets the placeholder, sends requests to ANTHROPIC_BASE_URL → api-proxy sidecar → sidecar injects the real key → forwards via Squid.

The dual-path write is purely a config pointer change; it does not alter what credentials the agent can access.

@github-actions
Copy link
Contributor

Smoke Test Results

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1414

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the agent startup configuration so Claude Code v2.1.81+ can find apiKeyHelper in its new config location while preserving compatibility with older versions.

Changes:

  • Refactors the Claude Code apiKeyHelper setup in entrypoint.sh into a helper function and writes it to both ~/.claude.json and ~/.claude/settings.json.
  • Updates an inline comment in docker-manager.ts to document the dual-path behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
containers/agent/entrypoint.sh Writes apiKeyHelper to both the legacy and new Claude Code config paths.
src/docker-manager.ts Comment update describing that both config paths are created by the entrypoint.
Comments suppressed due to low confidence (2)

containers/agent/entrypoint.sh:193

  • settings.json (and potentially ~/.claude.json) can contain multiple Claude settings. In the branch where the file exists but is missing apiKeyHelper, the script overwrites the entire JSON with { "apiKeyHelper": ... }, which will delete any existing user settings. Consider parsing the existing JSON and updating/adding only the apiKeyHelper key (e.g., via a small Node/Python helper), falling back to creating a new file only when the file does not exist or is empty/invalid.
        echo "[entrypoint] $label exists but missing apiKeyHelper, writing..."
        echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$config_file"
        chmod 666 "$config_file"
        echo "[entrypoint] ✓ Wrote apiKeyHelper to $label"
      fi
    else
      echo "[entrypoint] Creating $label with apiKeyHelper..."
      echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$config_file"
      chmod 666 "$config_file"

containers/agent/entrypoint.sh:204

  • chmod 777 "$SETTINGS_DIR" makes ~/.claude world-writable and, in chroot mode, will apply to the bind-mounted host ~/.claude directory. This can weaken filesystem isolation on the host and allows unintended modification of Claude state/config. Prefer ensuring correct ownership (mapped UID/GID) and using a restrictive directory mode (typically 700/755) instead of 777.
  mkdir -p "$SETTINGS_DIR"
  chmod 777 "$SETTINGS_DIR" 2>/dev/null || true
  write_api_key_helper "$SETTINGS_FILE" "$SETTINGS_FILE"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +174 to +182
if [ -f "$config_file" ]; then
if grep -q '"apiKeyHelper"' "$config_file"; then
CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$config_file" | cut -d'"' -f4)
if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then
echo "[entrypoint][ERROR] apiKeyHelper mismatch in $label:"
echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER"
echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER"
exit 1
fi
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grep -q '"apiKeyHelper"' check is looser than the subsequent extraction regex grep -o '"apiKeyHelper":"[^"]*"'. If the JSON is pretty-printed (e.g., whitespace around :) or uses escaped characters, the extraction can fail and, with set -e, may terminate the entrypoint unexpectedly. Consider using a JSON parser for both validation and update, or make the extraction robust to whitespace and handle parse failures by rewriting/updating the key instead of exiting due to a failed grep in a command substitution.

This issue also appears on line 185 of the same file.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +194
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$config_file"
chmod 666 "$config_file"
echo "[entrypoint] ✓ Wrote apiKeyHelper to $label"
fi
echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER"
else
# File exists but no apiKeyHelper - write it
echo "[entrypoint] Claude Code config file exists but missing apiKeyHelper, writing..."
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE"
chmod 666 "$CONFIG_FILE"
echo "[entrypoint] ✓ Wrote apiKeyHelper to $CONFIG_FILE"
echo "[entrypoint] Creating $label with apiKeyHelper..."
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$config_file"
chmod 666 "$config_file"
echo "[entrypoint] ✓ Created $label with apiKeyHelper: $CLAUDE_CODE_API_KEY_HELPER"
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chmod 666 on the config files makes them world-writable. Since these files control apiKeyHelper execution, leaving them writable by any user in the container is risky and also changes permissions on bind-mounted host files in chroot mode. Prefer chowning the file to the runtime user (awfuser / mapped UID) and using a least-privilege mode (e.g., 600/644 depending on whether Claude needs to update it).

This issue also appears on line 202 of the same file.

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

* Initial plan

* fix: preserve existing claude config when merging apiKeyHelper

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/6efe3da1-78c0-44c2-b7ce-06b070ab91b8

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
@github-actions
Copy link
Contributor

Smoke Test Results

Test Result
GitHub MCP (last 2 merged PRs: #1418, #1405)
Playwright (github.com title contains "GitHub")
File write (smoke-test-claude-23507729080.txt)
Bash verification (cat file)

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1414

@github-actions

This comment has been minimized.

…abled (#1419)

* Initial plan

* fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled

Regression introduced in commit 9761e87 (PR #1305) re-added an unconditional
GITHUB_API_URL assignment that PR #1303 had fixed. Combined with the missing
EXCLUDED_ENV_VARS entry for the --env-all code path, GITHUB_API_URL was leaking
into the agent container when api-proxy is enabled.

With GITHUB_API_URL present in the agent, the Copilot CLI routes its token
exchange to api.github.com/copilot_internal/v2/token using the placeholder
COPILOT_GITHUB_TOKEN instead of going through COPILOT_API_URL → api-proxy
(which injects the real token), causing a 401 authentication failure.

Fix:
- Add GITHUB_API_URL to EXCLUDED_ENV_VARS when api-proxy is enabled (covers
  the --env-all path used by build-test.lock.yml)
- Replace the duplicate unconditional assignment with the correct conditional
  version that was already present below it (from PR #1303)

Tests:
- should not leak GITHUB_API_URL to agent when api-proxy is enabled with envAll
- should pass GITHUB_API_URL to agent when api-proxy is NOT enabled with envAll

See: github/gh-aw#20875

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@github-actions
Copy link
Contributor

Smoke Test Results — PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1414

@github-actions

This comment has been minimized.

…led (#1420)

* Initial plan

* fix: restore GITHUB_API_URL in agent container when api-proxy is enabled

Root cause: commit 5a56789 excluded GITHUB_API_URL from the agent container
when api-proxy is enabled, based on the incorrect theory that its presence
would cause the Copilot CLI to route token exchange through api.github.com
with the placeholder token.

Evidence from CI logs: successful runs (worktree-audit-observability) had
GITHUB_API_URL present and worked; all failing runs lacked it and failed
immediately with "Authentication failed" after ~1.2 seconds.

The Copilot CLI needs GITHUB_API_URL to locate the GitHub API. Its
Copilot-specific calls (token exchange, inference) already route through
COPILOT_API_URL → api-proxy regardless of GITHUB_API_URL being set.

Fix:
- Remove EXCLUDED_ENV_VARS.add('GITHUB_API_URL') from api-proxy block
- Change conditional GITHUB_API_URL assignment to unconditional
- Update test to expect GITHUB_API_URL present even with api-proxy enabled

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/db4a1afe-02b9-43f2-bfae-41febbcbc8b5

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
@github-actions
Copy link
Contributor

Smoke test results — run 23512312431

✅ GitHub MCP: PR #1420 "fix: restore GITHUB_API_URL in agent container when api-proxy is enabled", PR #1419 "fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled"
✅ Playwright: github.com title contains "GitHub"
✅ File write: /tmp/gh-aw/agent/smoke-test-claude-23512312431.txt created
✅ Bash verify: file contents confirmed

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1414

@github-actions
Copy link
Contributor

🔮 The ancient spirits stir; smoke omens are read.
fix: restore GITHUB_API_URL in agent container when api-proxy is enabled
fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled
GitHub MCP (last 2 merged PRs): ✅
safeinputs-gh PR query (2 PRs): ❌ (tool unavailable)
Playwright title contains "GitHub": ✅
Tavily search results returned: ❌ (tool unavailable)
File write + bash cat verify: ✅
Discussion query/comment: ❌ (tool unavailable)
Build (npm ci && npm run build): ✅ | Overall: FAIL

🔮 The oracle has spoken through Smoke Codex

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • ab.chatgpt.com
  • registry.npmjs.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"
    - "registry.npmjs.org"

See Network Configuration for more information.

@github-actions
Copy link
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia 1/1 passed ✅ PASS
Bun hono 1/1 passed ✅ PASS
C++ fmt N/A ✅ PASS
C++ json N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world N/A ✅ PASS
.NET json-parse N/A ✅ PASS
Go color 1/1 passed ✅ PASS
Go env 1/1 passed ✅ PASS
Go uuid 1/1 passed ✅ PASS
Java gson 1/1 passed ✅ PASS
Java caffeine 1/1 passed ✅ PASS
Node.js clsx All passed ✅ PASS
Node.js execa All passed ✅ PASS
Node.js p-limit All passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Generated by Build Test Suite for issue #1414 ·

@github-actions
Copy link
Contributor

Smoke Test Results — PASS

Test Result
GitHub MCP (last 2 merged PRs)
Playwright (github.com title check)
File write
Bash verification

Last 2 merged PRs:

PR author: @lpcox · No assignees

📰 BREAKING: Report filed by Smoke Copilot for issue #1414

@github-actions
Copy link
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Result: Not all runtimes matched — smoke-chroot label not applied.

Tested by Smoke Chroot for issue #1414

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine: claude — ANTHROPIC_API_KEY not reaching Claude Code CLI inside sandbox (apiKeySource: none)

4 participants