# PRD 0071: Claude forward_host_credentials

- **Status:** Active
- **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/.credentials.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/.credentials.json`
after `claude login`. 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/.credentials.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": "<access-token>",
    "refreshToken": "<refresh-token>",
    "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/.credentials.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.
