Files
bot-bottle/docs/prds/prd-new-claude-forward-host-credentials.md
T
didericis-claude 7278ee1157
lint / lint (push) Failing after 2m10s
test / unit (pull_request) Successful in 57s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Successful in 1m6s
fix(claude): fall back to macOS Keychain for credentials
On macOS, Claude Code stores credentials in the Keychain under
service "Claude Code-credentials" rather than in a file. When
~/.claude/.credentials.json is absent, shell out to:
  security find-generic-password -s "Claude Code-credentials" -w
and parse the result as the same JSON schema.

~/.claude.json holds only profile/UI metadata (oauthAccount has
no token fields). expiresAt in the credentials is milliseconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:46:53 +00:00

5.0 KiB

PRD prd-new: Claude forward_host_credentials

  • Status: Draft
  • Author: claude
  • Created: 2026-07-01
  • Issue: #325

Summary

Add agent_provider.forward_host_credentials: true support for the claude template, mirroring the existing Codex flow. When enabled, bot-bottle reads the host's Claude OAuth session key from ~/.claude.json at launch, forwards it only to the egress sidecar, and injects a placeholder CLAUDE_CODE_OAUTH_TOKEN into the agent so Claude Code starts without ever seeing the real credential.

Problem

Running a Claude agent in a container today requires the operator to manually extract a long-lived OAuth token (claude setup-token), export it as BOT_BOTTLE_CLAUDE_OAUTH_TOKEN, and reference it explicitly in the manifest with agent_provider.auth_token: "BOT_BOTTLE_CLAUDE_OAUTH_TOKEN". This is a two-step manual ceremony that is easy to skip or do incorrectly.

The host already stores a valid Claude session in ~/.claude.json after claude login or claude setup-token. Codex already automates an equivalent extraction from ~/.codex/auth.json. There is no reason Claude bottles cannot do the same.

Goals / Success Criteria

  • A Claude bottle with forward_host_credentials: true in the manifest uses the host's ~/.claude.json session key at launch with no additional operator steps.
  • The agent container receives only CLAUDE_CODE_OAUTH_TOKEN=egress-placeholder — never the real token.
  • The real session key lives only in the egress sidecar's environment.
  • Missing, malformed, or expired host Claude auth fails launch with a clear operator-facing message.
  • Existing auth_token behavior is unchanged.
  • forward_host_credentials: true is rejected in the manifest when both auth_token and forward_host_credentials are set, since they serve the same purpose.

Non-goals

  • Refreshing Claude OAuth tokens in the sidecar.
  • Writing a dummy ~/.claude.json auth state to the agent (unlike the Codex flow, Claude Code reads its credential from CLAUDE_CODE_OAUTH_TOKEN in env, not from an auth file — no guest-side auth marker is needed).
  • Supporting forward_host_credentials for providers other than codex and claude.

Design

Manifest schema

agent_provider:
  template: claude
  forward_host_credentials: true

Rejects in manifest validation when:

  • Template is not codex or claude.
  • Both auth_token and forward_host_credentials are set.

Host auth extraction (contrib/claude/claude_auth.py)

Claude Code credential storage varies by platform:

  • Linux: ~/.claude/.credentials.json
  • macOS: macOS Keychain, service "Claude Code-credentials" (the file path is tried first; Keychain is the fallback when the file is absent)

~/.claude.json contains only UI state and profile metadata — no token.

The credentials JSON schema (same whether from file or Keychain):

{
  "claudeAiOauth": {
    "accessToken": "sk-ant-oat01-...",
    "refreshToken": "sk-ant-ort01-...",
    "expiresAt": 1748276587173,
    "scopes": ["user:inference", "user:profile"]
  }
}

expiresAt is in milliseconds (not seconds).

At prepare/launch time, when forward_host_credentials: true:

  1. Try ~/.claude/.credentials.json; on macOS, if absent, run security find-generic-password -s "Claude Code-credentials" -w and parse its stdout as JSON.
  2. Require a claudeAiOauth dict.
  3. Require a non-empty claudeAiOauth.accessToken string.
  4. If claudeAiOauth.expiresAt is present, divide by 1000 and require the result to be in the future.
  5. Return only the access token to the launch path.

Errors name the missing or invalid condition and point the operator at claude login, without printing token values.

Egress route

When forward_host_credentials: true:

  • Provision the session key in provisioned_env under BOT_BOTTLE_CLAUDE_HOST_ACCESS_TOKEN (new constant in egress.py).
  • Set up the api.anthropic.com egress route with auth_scheme: Bearer and token_ref: BOT_BOTTLE_CLAUDE_HOST_ACCESS_TOKEN.
  • Set CLAUDE_CODE_OAUTH_TOKEN=egress-placeholder in the agent env and add it to hidden_env_names.

No dummy auth file and no verify step are needed — Claude Code reads the credential from the env var, not from a file.

Constants

  • CLAUDE_HOST_CREDENTIAL_TOKEN_REF = "BOT_BOTTLE_CLAUDE_HOST_ACCESS_TOKEN" in egress.py (alongside the existing CODEX_HOST_CREDENTIAL_TOKEN_REF).
  • CLAUDE_HOST_CREDENTIAL_HOSTS = ("api.anthropic.com",) in agent_provider.py (alongside the existing CODEX_HOST_CREDENTIAL_HOSTS).

Data flow

Host ~/.claude.json  →  bot-bottle launch
                            │
                            ├──► egress sidecar env (real token only)
                            │
                            └──► agent env: CLAUDE_CODE_OAUTH_TOKEN=egress-placeholder

Agent  →  HTTPS to api.anthropic.com (via egress)
Egress →  injects Authorization: Bearer <real token>
Egress →  forwards to api.anthropic.com

Open questions

None — the Codex precedent makes the design clear.