docs(agent): clarify claude oauth env
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 43s

This commit is contained in:
2026-05-28 18:20:09 -04:00
parent cacba087c9
commit cdb1870b1c
14 changed files with 41 additions and 40 deletions
+22 -21
View File
@@ -313,7 +313,7 @@ egress:
role: claude_code_oauth role: claude_code_oauth
auth: auth:
scheme: Bearer scheme: Bearer
token_ref: BOT_BOTTLE_OAUTH_TOKEN token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
--- ---
Common Claude provider boundary. Common Claude provider boundary.
@@ -389,11 +389,12 @@ Working examples live under `examples/`. Pipelock's design lives in
rationale in `docs/research/pipelock-assessment.md`. The trust rationale in `docs/research/pipelock-assessment.md`. The trust
boundary rationale lives in `docs/prds/0011-per-file-md-manifest.md`. boundary rationale lives in `docs/prds/0011-per-file-md-manifest.md`.
## Auth: OAuth token, not API key ## Auth: Claude OAuth token, not API key
bot-bottle authenticates `claude` inside the container with the same Bottles that use `agent_provider.template: claude` authenticate
Pro/Max subscription you already use on the host, via a long-lived OAuth `claude` inside the container with the same Pro/Max subscription you
token. No `ANTHROPIC_API_KEY` is needed. already use on the host, via a long-lived OAuth token. No
`ANTHROPIC_API_KEY` is needed.
**Why a token instead of mounting `~/.claude.json`:** on macOS, Claude **Why a token instead of mounting `~/.claude.json`:** on macOS, Claude
Code stores OAuth credentials in the encrypted Keychain, not in Code stores OAuth credentials in the encrypted Keychain, not in
@@ -409,28 +410,28 @@ claude setup-token # browser login, prints a ~1-year OAuth token
``` ```
Stash the token in your shell env (e.g. `~/.zshrc` or a secret manager) Stash the token in your shell env (e.g. `~/.zshrc` or a secret manager)
as `BOT_BOTTLE_OAUTH_TOKEN`: as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`:
```sh ```sh
export BOT_BOTTLE_OAUTH_TOKEN="<token>" export BOT_BOTTLE_CLAUDE_OAUTH_TOKEN="<token>"
``` ```
The bottle reaches the Anthropic API only through the cred-proxy The Claude bottle reaches the Anthropic API only through the cred-proxy
sidecar. To let `claude` authenticate, declare a route in sidecar. To let `claude` authenticate, declare an egress route with
`bottle.cred_proxy.routes` with `role: "anthropic-base-url"` and `role: claude_code_oauth` and
`token_ref: "BOT_BOTTLE_OAUTH_TOKEN"`: `token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`:
```jsonc ```yaml
{ egress:
"path": "/anthropic/", routes:
"upstream": "https://api.anthropic.com", - host: api.anthropic.com
"auth_scheme": "Bearer", role: claude_code_oauth
"token_ref": "BOT_BOTTLE_OAUTH_TOKEN", auth:
"role": "anthropic-base-url" scheme: Bearer
} token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
``` ```
At launch, `cli.py` reads `BOT_BOTTLE_OAUTH_TOKEN` from the host At launch, `cli.py` reads `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` from the host
env and forwards it into the cred-proxy container's environ — never env and forwards it into the cred-proxy container's environ — never
into the agent's. The agent receives `ANTHROPIC_BASE_URL` pointing at into the agent's. The agent receives `ANTHROPIC_BASE_URL` pointing at
`http://cred-proxy:9099/anthropic` and a non-secret placeholder for `http://cred-proxy:9099/anthropic` and a non-secret placeholder for
@@ -439,7 +440,7 @@ the proxy strips and replaces the header on every request). `printenv`
inside the agent does not surface the real token, and the value is inside the agent does not surface the real token, and the value is
never written to disk or placed on argv on the host. never written to disk or placed on argv on the host.
A bottle without an `anthropic-base-url` route has no path to the A Claude bottle without a `claude_code_oauth` route has no path to the
Anthropic API — there is no fallback that forwards the token directly Anthropic API — there is no fallback that forwards the token directly
to the agent. Caveats: the token is bound to your subscription tier to the agent. Caveats: the token is bound to your subscription tier
(Pro/Max/Team/Enterprise), it does not work with `claude --bare` (Pro/Max/Team/Enterprise), it does not work with `claude --bare`
+1 -1
View File
@@ -26,7 +26,7 @@ entry and pushes straight at gitea/github with ssh-gate doing dumb
L4 forwarding. There is no boundary between "the agent thinks this L4 forwarding. There is no boundary between "the agent thinks this
commit is fine" and "the secret hits an external remote." If a commit is fine" and "the secret hits an external remote." If a
compromised or careless agent stages a `.env`, slips a token into compromised or careless agent stages a `.env`, slips a token into
a fixture, or commits the `BOT_BOTTLE_OAUTH_TOKEN` itself, `git a fixture, or commits the `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` itself, `git
push` ships it. push` ships it.
Host-side pre-commit / pre-push hooks are the usual defense, but Host-side pre-commit / pre-push hooks are the usual defense, but
+2 -2
View File
@@ -230,7 +230,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as
``` ```
┌── Host (macOS) ──────────────────────────────────────────────────┐ ┌── Host (macOS) ──────────────────────────────────────────────────┐
│ Secrets at rest (keychain / .env): │ │ Secrets at rest (keychain / .env): │
│ BOT_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN, │ │ BOT_BOTTLE_CLAUDE_OAUTH_TOKEN, GITHUB_TOKEN, │
│ GITEA_SERVER_TOKEN, NPM_TOKEN │ │ GITEA_SERVER_TOKEN, NPM_TOKEN │
│ │ docker run -e KEY (no =VALUE on argv) │ │ │ docker run -e KEY (no =VALUE on argv) │
│ ▼ │ │ ▼ │
@@ -315,7 +315,7 @@ Why the agent can't reach the sidecar's environ:
+ validate route shape, role enum, path uniqueness, singleton- + validate route shape, role enum, path uniqueness, singleton-
role constraints. role constraints.
- **`bot_bottle/backend/docker/prepare.py`** — drop the - **`bot_bottle/backend/docker/prepare.py`** — drop the
legacy `BOT_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN` legacy `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN`
forward entirely. cred-proxy is the only path the Anthropic forward entirely. cred-proxy is the only path the Anthropic
OAuth token reaches the bottle. When a route claims the OAuth token reaches the bottle. When a route claims the
`anthropic-base-url` role, write `ANTHROPIC_BASE_URL` `anthropic-base-url` role, write `ANTHROPIC_BASE_URL`
+1 -1
View File
@@ -261,7 +261,7 @@ cred_proxy:
- path: /anthropic/ - path: /anthropic/
upstream: https://api.anthropic.com upstream: https://api.anthropic.com
auth_scheme: Bearer auth_scheme: Bearer
token_ref: BOT_BOTTLE_OAUTH_TOKEN token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
role: anthropic-base-url role: anthropic-base-url
- path: /gitea/dideric/ - path: /gitea/dideric/
upstream: https://gitea.dideric.is upstream: https://gitea.dideric.is
@@ -78,7 +78,7 @@ The remaining credible designs reduce to three:
### Anthropic / Claude Code ### Anthropic / Claude Code
**Today's wiring** (`bot_bottle/cli/start.py`): the host's **Today's wiring** (`bot_bottle/cli/start.py`): the host's
`BOT_BOTTLE_OAUTH_TOKEN` is forwarded into the bottle as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` is forwarded into the bottle as
`CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e CLAUDE_CODE_OAUTH_TOKEN` `CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e CLAUDE_CODE_OAUTH_TOKEN`
(no `=value`, so the value never lands on argv — good). Inside the (no `=value`, so the value never lands on argv — good). Inside the
bottle, claude runs as `node` (UID 1000) with bottle, claude runs as `node` (UID 1000) with
@@ -63,7 +63,7 @@ For a known-leaked or suspected-leaked token:
1. Revoke the entry at `claude.ai/settings/claude-code`. 1. Revoke the entry at `claude.ai/settings/claude-code`.
2. Run "Log out all sessions" under Settings → Account → Active Sessions. 2. Run "Log out all sessions" under Settings → Account → Active Sessions.
3. Run `claude setup-token` to mint a replacement, and rotate it into 3. Run `claude setup-token` to mint a replacement, and rotate it into
`BOT_BOTTLE_OAUTH_TOKEN` immediately. `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` immediately.
4. Email Anthropic support at `support.anthropic.com`. Security issues 4. Email Anthropic support at `support.anthropic.com`. Security issues
sometimes get attention that GitHub issues do not. sometimes get attention that GitHub issues do not.
@@ -88,7 +88,7 @@ already on the attacker's box. Detection has to be at *commit* time
Two surfaces are exposed: Two surfaces are exposed:
1. **The bot-bottle repo itself.** Development happens on a host 1. **The bot-bottle repo itself.** Development happens on a host
with `BOT_BOTTLE_OAUTH_TOKEN`, Gitea tokens, and other with `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, Gitea tokens, and other
credentials in the environment. A fixture, test snapshot, log credentials in the environment. A fixture, test snapshot, log
capture, or pasted-in debug output could carry one of them into a capture, or pasted-in debug output could carry one of them into a
tracked file. The repo's Gitea remote is private, but mirrors or tracked file. The repo's Gitea remote is private, but mirrors or
@@ -254,7 +254,7 @@ cred_proxy:
- path: /anthropic/ - path: /anthropic/
upstream: https://api.anthropic.com upstream: https://api.anthropic.com
auth_scheme: Bearer auth_scheme: Bearer
token_ref: BOT_BOTTLE_OAUTH_TOKEN token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
role: anthropic-base-url role: anthropic-base-url
egress: egress:
allowlist: [example.com] allowlist: [example.com]
+1 -1
View File
@@ -33,7 +33,7 @@ on top of working onboarding.
### Onboarding friction ### Onboarding friction
A first-time user today goes through five steps: install Docker, A first-time user today goes through five steps: install Docker,
install `uv`, set `BOT_BOTTLE_OAUTH_TOKEN`, write install `uv`, set `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, write
`bot-bottle.json`, run `./cli.py start`. One of those is `bot-bottle.json`, run `./cli.py start`. One of those is
"author a JSON manifest." Polished tools in this category let "author a JSON manifest." Polished tools in this category let
users skip that step on day one. The fix is an `init` subcommand users skip that step on day one. The fix is an `init` subcommand
+3 -3
View File
@@ -102,7 +102,7 @@ work:
- **Typing latency.** Interactive Claude sessions over SSH have visible - **Typing latency.** Interactive Claude sessions over SSH have visible
per-keystroke latency; usually fine on wired/fiber, less fine on per-keystroke latency; usually fine on wired/fiber, less fine on
Wi-Fi-to-cloud. Mosh helps if it's bothersome. Wi-Fi-to-cloud. Mosh helps if it's bothersome.
- **Token shipping.** `BOT_BOTTLE_OAUTH_TOKEN` has to live on the - **Token shipping.** `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` has to live on the
remote box for the launcher to forward it into containers. Use the remote box for the launcher to forward it into containers. Use the
provider's secret-injection path (cloud-init user-data, provider's secret-injection path (cloud-init user-data,
`flyctl secrets`, Tailscale-served local file, etc.). Never echo the `flyctl secrets`, Tailscale-served local file, etc.). Never echo the
@@ -130,7 +130,7 @@ The minimum-viable workflow, no bot-bottle code changes:
(`curl -fsSL https://get.docker.com | sh`). (`curl -fsSL https://get.docker.com | sh`).
3. SSH in. 3. SSH in.
4. `git clone` bot-bottle on the VM, drop a manifest in place, 4. `git clone` bot-bottle on the VM, drop a manifest in place,
inject `BOT_BOTTLE_OAUTH_TOKEN` via the provider's secrets path. inject `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` via the provider's secrets path.
5. `./cli.py start <agent>` — the existing launcher handles the rest. 5. `./cli.py start <agent>` — the existing launcher handles the rest.
6. On exit: destroy the VM. No host artifacts persist. 6. On exit: destroy the VM. No host artifacts persist.
@@ -157,7 +157,7 @@ Build a custom OCI image `FROM docker:dind` that bakes in:
the dind storage. Without this step, the first in-VM `docker build` the dind storage. Without this step, the first in-VM `docker build`
runs `apt-get` and a global `npm install -g runs `apt-get` and a global `npm install -g
@anthropic-ai/claude-code`, which adds 3090 s to every cold start. @anthropic-ai/claude-code`, which adds 3090 s to every cold start.
- A `flyctl secrets`-injected `BOT_BOTTLE_OAUTH_TOKEN`, exposed to - A `flyctl secrets`-injected `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, exposed to
the VM's PID 1 as an env var. the VM's PID 1 as an env var.
- An entrypoint that starts dockerd, waits for it to be healthy, then - An entrypoint that starts dockerd, waits for it to be healthy, then
either drops into a shell or directly runs `cli.py start <agent>`. either drops into a shell or directly runs `cli.py start <agent>`.
@@ -79,7 +79,7 @@ The agent's conversation channel is therefore wide open as an exfil
path. A prompt-injected agent that has been told a secret can ship path. A prompt-injected agent that has been told a secret can ship
it to Anthropic as conversation text, formatted however it likes, it to Anthropic as conversation text, formatted however it likes,
and pipelock sees only `CONNECT api.anthropic.com:443`. The and pipelock sees only `CONNECT api.anthropic.com:443`. The
`BOT_BOTTLE_OAUTH_TOKEN` itself rides this exact path. `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` itself rides this exact path.
### 3. Out-of-band channels exist regardless ### 3. Out-of-band channels exist regardless
@@ -134,7 +134,7 @@ per-bottle gate that:
Two concrete instances worth implementing: Two concrete instances worth implementing:
**Anthropic-API gate.** Holds `BOT_BOTTLE_OAUTH_TOKEN`. Agent's **Anthropic-API gate.** Holds `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`. Agent's
`ANTHROPIC_BASE_URL` points at the gate; gate injects `ANTHROPIC_BASE_URL` points at the gate; gate injects
`Authorization: Bearer …` and forwards to api.anthropic.com. The `Authorization: Bearer …` and forwards to api.anthropic.com. The
token is no longer in the bottle's env. Once the token is out, token is no longer in the bottle's env. Once the token is out,
+1 -1
View File
@@ -8,7 +8,7 @@ egress:
role: claude_code_oauth role: claude_code_oauth
auth: auth:
scheme: Bearer scheme: Bearer
token_ref: BOT_BOTTLE_OAUTH_TOKEN token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
--- ---
Common Claude provider boundary. Drop this file into Common Claude provider boundary. Drop this file into
+2 -2
View File
@@ -11,8 +11,8 @@ if ! command -v vhs >/dev/null 2>&1; then
exit 1 exit 1
fi fi
if [ -z "${BOT_BOTTLE_OAUTH_TOKEN:-}" ]; then if [ -z "${BOT_BOTTLE_CLAUDE_OAUTH_TOKEN:-}" ]; then
echo "demo-record: BOT_BOTTLE_OAUTH_TOKEN is unset; claude inside the bottle will not auth" >&2 echo "demo-record: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN is unset; claude inside the bottle will not auth" >&2
exit 1 exit 1
fi fi
+2 -2
View File
@@ -14,9 +14,9 @@ set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ -z "${BOT_BOTTLE_OAUTH_TOKEN:-}" ]; then if [ -z "${BOT_BOTTLE_CLAUDE_OAUTH_TOKEN:-}" ]; then
cat <<'EOF' >&2 cat <<'EOF' >&2
demo: BOT_BOTTLE_OAUTH_TOKEN is unset. The bottle launches claude, demo: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN is unset. The bottle launches claude,
which needs the token to authenticate. Set it in your shell env (e.g. which needs the token to authenticate. Set it in your shell env (e.g.
~/.zshrc) — see README §Auth — then re-run. ~/.zshrc) — see README §Auth — then re-run.
EOF EOF