7278ee1157
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>
147 lines
5.0 KiB
Markdown
147 lines
5.0 KiB
Markdown
# 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
|
|
|
|
```yaml
|
|
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):
|
|
|
|
```json
|
|
{
|
|
"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.
|