The actual OAuth token is in ~/.claude/.credentials.json under claudeAiOauth.accessToken, not in ~/.claude.json. ~/.claude.json holds only UI state and profile metadata (oauthAccount has no token fields). expiresAt in the credentials file is milliseconds, not seconds. Discovered after testing against Claude Code 2.1.198. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.8 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: truein the manifest uses the host's~/.claude.jsonsession 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_tokenbehavior is unchanged. forward_host_credentials: trueis rejected in the manifest when bothauth_tokenandforward_host_credentialsare set, since they serve the same purpose.
Non-goals
- Refreshing Claude OAuth tokens in the sidecar.
- Writing a dummy
~/.claude.jsonauth state to the agent (unlike the Codex flow, Claude Code reads its credential fromCLAUDE_CODE_OAUTH_TOKENin env, not from an auth file — no guest-side auth marker is needed). - Supporting
forward_host_credentialsfor providers other thancodexandclaude.
Design
Manifest schema
agent_provider:
template: claude
forward_host_credentials: true
Rejects in manifest validation when:
- Template is not
codexorclaude. - Both
auth_tokenandforward_host_credentialsare set.
Host auth extraction (contrib/claude/claude_auth.py)
Claude Code stores its OAuth credentials in ~/.claude/.credentials.json
(not in ~/.claude.json, which contains only UI state and profile
metadata). The relevant fields are:
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"expiresAt": 1748276587173,
"scopes": ["user:inference", "user:profile"]
}
}
Note: expiresAt is in milliseconds (not seconds).
At prepare/launch time, when forward_host_credentials: true:
- Resolve
~/.claude/.credentials.json(via$HOME). - Parse the JSON object.
- Require a
claudeAiOauthdict. - Require a non-empty
claudeAiOauth.accessTokenstring. - If
claudeAiOauth.expiresAtis present as a number, divide by 1000 and require the result to be in the future. - 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_envunderBOT_BOTTLE_CLAUDE_HOST_ACCESS_TOKEN(new constant inegress.py). - Set up the
api.anthropic.comegress route withauth_scheme: Bearerandtoken_ref: BOT_BOTTLE_CLAUDE_HOST_ACCESS_TOKEN. - Set
CLAUDE_CODE_OAUTH_TOKEN=egress-placeholderin the agent env and add it tohidden_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"inegress.py(alongside the existingCODEX_HOST_CREDENTIAL_TOKEN_REF).CLAUDE_HOST_CREDENTIAL_HOSTS = ("api.anthropic.com",)inagent_provider.py(alongside the existingCODEX_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.