Cleans up references to the pre-refactor bash layout (cli.sh, lib/*.sh, scripts/*.sh) across README, Dockerfile, the pipelock PRD, and research notes. Refreshes line numbers in the oauth-token note against the current cli/start.py. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
8.6 KiB
OAuth token exposure to claude inside the bottle
Research into whether CLAUDE_CODE_OAUTH_TOKEN — as currently forwarded into
each claude-bottle container — is reachable by claude itself, what (if any)
deeper-than-prompt mechanisms could hide it, and whether routing claude
through an auth-injecting proxy is viable.
Summary
Yes, claude can read CLAUDE_CODE_OAUTH_TOKEN trivially today. There is no
Linux primitive for "this env var exists in my process but I cannot read it";
the only credible boundary is to put the credential in a different process
that claude cannot read. Default Docker enforces that boundary at the kernel
level (a non-root process cannot read /proc/<root-pid>/environ), so a
root-owned auth-injecting reverse proxy listening on 127.0.0.1 is a
realistic design. Claude Code's ANTHROPIC_BASE_URL officially supports
this routing pattern with bearer auth, with documented caveats around SSE,
header passthrough, and out-of-band outbound traffic (telemetry, npm, etc.)
that does not route through ANTHROPIC_BASE_URL at all.
How the token reaches claude today
claude_bottle/cli/start.py(around line 237–238) — host'sCLAUDE_BOTTLE_OAUTH_TOKENis exported into the launcher process asCLAUDE_CODE_OAUTH_TOKEN, then forwarded withdocker run -e CLAUDE_CODE_OAUTH_TOKEN(no=value, so the value never lands on argv — good).claude_bottle/cli/start.py(around line 318–325) — claude is launched viadocker exec -it <container> claude …, which inherits the container PID 1's env, including the token.- claude runs as
node(UID 1000) with--dangerously-skip-permissions. Its Bash tool can runprintenv CLAUDE_CODE_OAUTH_TOKEN,cat /proc/self/environ,node -e 'console.log(process.env)', etc. and capture the value into the conversation.
A prompt-injection vector — a poisoned skill, a malicious string in a file
claude reads, or a hijacked MCP server — can extract the token and
exfiltrate it through any allowed outbound channel. The
local-vs-remote-agent-execution.md note already flags static long-lived
tokens as the biggest credential risk; this is exactly that risk, present
in the local topology today.
Hiding env vars "at a deeper level"
Linux has no primitive to mark an individual env var as unreadable to the
process that holds it. Once a var is in a process's environ, the process
and its descendants have full access. The deeper-level lever is process
boundary, not env-var ACL: put the credential in a different process
that claude cannot read.
Default Docker enforces this for you. The kernel's ptrace_may_access
check rejects /proc/<pid>/environ reads when the caller's UID/GID don't
match the target's and the caller lacks CAP_SYS_PTRACE or CAP_PERFMON.
A node-uid claude process attempting to read a root-owned proxy's
environ gets EACCES. Escape hatches are explicit and not used by
claude-bottle: --cap-add=SYS_PTRACE, --cap-add=PERFMON,
--privileged. Yama ptrace_scope is irrelevant here because it only
relaxes the same-UID relationship check; the cross-UID UID-match
requirement still blocks the read.
The apiKeyHelper setting in claude-code is not a boundary. The
helper is invoked by claude's own process, so claude can just call it via
Bash and capture stdout. Same trust domain.
The only credible designs:
- Header-injecting reverse proxy — claude points at a localhost URL;
proxy holds the credential; proxy adds
Authorization: Bearerand forwards. (See next section.) - Network namespace + outbound proxy — claude runs with
--network noneand a unix-socket proxy that holds the credential and enforces an egress allowlist. Anthropic's secure-deployment docs describe this pattern; the existing research note on remote agents recommends adding it locally first as the highest-leverage change. - Don't ship the OAuth token at all — fall back to per-session login or short-lived tokens. Operationally heavier, and the long-lived OAuth token is the chosen design here precisely because it's portable across hosts (Keychain on macOS, file on Linux).
Proxy auth: viable, with caveats
Pattern:
- Run a small reverse proxy as root inside the container, listening on
127.0.0.1:N(or a root-owned unix socket withSO_PEERCREDchecks). - Set
ANTHROPIC_BASE_URL=http://127.0.0.1:N(or the socket path) in claude's env. Claude asnodeonly sees the URL, not the token. - The proxy injects
Authorization: Bearer $TOKENand forwards tohttps://api.anthropic.com. - Token lives only in the root proxy's env; node-uid claude cannot read
/proc/<root-pid>/environ(kernel-enforced).
ANTHROPIC_BASE_URL is documented as routing for proxies/gateways, not
just Bedrock/Vertex, and works alongside bearer auth. Confirmed gotchas:
-
SSE streaming: proxy must not buffer responses (nginx
proxy_buffering off, or use a streaming-aware proxy). Claude Code uses SSE only — no websockets. -
Forward
anthropic-version,anthropic-beta, andX-Claude-Code-Session-Iduntouched — stripping them breaks tool use / extended thinking / session aggregation. -
GitHub issue #36998: interactive mode historically bypassed
ANTHROPIC_BASE_URLfor some startup calls (auth validation / org lookup), connecting directly toapi.anthropic.com. Marked closed but verify withtcpdumporstrace -e connectagainst the pinned 2.1.126 build before trusting the isolation. -
Tool search (
ENABLE_TOOL_SEARCH): disabled by default whenANTHROPIC_BASE_URLis non-Anthropic; re-enable explicitly if needed. -
Out-of-band outbound traffic is the weak link. None of these route through
ANTHROPIC_BASE_URL:statsig.anthropic.com— telemetry (disable:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1,DISABLE_TELEMETRY=1)- Sentry error reporting (disable:
DISABLE_ERROR_REPORTING=1) registry.npmjs.org,github.com,release-assets.githubusercontent.com— MCP installs and autoupdaterpypi.org,bun.sh— if the Bash tool installs Python or Bun packages during a session
A hijacked claude could exfiltrate the captured token (or any other data) through these channels even with the proxy in place. Pair the proxy with an explicit egress allowlist (iptables / Docker network policy) for the full benefit.
-
Token refresh:
claude setup-tokenissues a ~1-year token with no client-side refresh, so a static proxy value is fine. -
No request signing / anti-replay on the Messages API; header rewriting is safe.
-
--baremode does not readCLAUDE_CODE_OAUTH_TOKENat all (onlyANTHROPIC_API_KEY). Not relevant to the interactive flow claude-bottle ships, but worth noting if--bareis ever wired in.
Recommended path forward
In priority order:
--network none+ a localhost (or unix-socket) auth-injecting proxy that holds the token. Highest-leverage change: credential isolation and egress containment in one pass. Aligns with the recommendation already inlocal-vs-remote-agent-execution.md.- Layer in
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1plus an explicit egress allowlist (api.anthropic.com only, plus the per-agent set of MCP / git / package-registry hosts) so a hijacked claude can't exfiltrate through statsig / Sentry / npm.
The current docker run -e CLAUDE_CODE_OAUTH_TOKEN pattern is fine for
argv hygiene on the host, but inside the container the token is fully
exposed to claude. The proxy pattern moves it across a real
kernel-enforced boundary.
Sources
- Authentication — Claude Code Docs
- LLM gateway configuration — Claude Code Docs
- Claude Code Environment Variables
- GitHub issue #36998 — Interactive mode ignores ANTHROPIC_BASE_URL
- GitHub issue #11587 — Auth conflict: CLAUDE_CODE_OAUTH_TOKEN and apiKeyHelper
- proc_pid_environ(5) Linux manual page
- Documenting ptrace access mode checking — LWN.net
- StepSecurity — Claude Code Action outbound network analysis
- Manage API key environment variables — Claude Help Center