Assisted-by: Codex
23 KiB
Agent credential proxy landscape
Consolidated research on running an auth-header-injecting proxy in front of an AI agent so API tokens stay out of the agent's process space. Folds in the per-service mechanics for the Anthropic OAuth token and the Gitea PAT — the two cases bot-bottle hits first — and surveys existing tools as of May 2026.
Companion to
secret-minimization-over-dlp.md
(the architectural framing — why this matters), and to
local-vs-remote-agent-execution.md
(the broader threat model that flagged long-lived static tokens as
the biggest credential risk).
Summary
Today every bot-bottle agent gets CLAUDE_CODE_OAUTH_TOKEN (and
any bottle.env secrets like a Gitea PAT) injected as env vars,
which means the agent process can read them with printenv or
/proc/self/environ. A prompt-injected or hijacked agent can ship
those bytes to any allowed host. Linux has no primitive for
"this env var exists in my process but I can't read it" — the only
credible boundary is to put the credential in a different process
that the agent cannot read, and let the agent talk to it over a
narrow API. Default Docker enforces that boundary at the kernel
level via ptrace_may_access; a future smolmachines backend
enforces it harder, at the VM line.
Several existing tools implement this pattern, but none of them are a clean drop-in for bot-bottle today: the most architecturally aligned (nono) is alpha; the most mature open-source (Infisical Agent Vault) requires TLS MITM and would double up on pipelock's TLS-interception stack. For the Anthropic-token slice, a small bot-bottle-specific reverse proxy modeled on the phantom-token shape is probably the right call. For Gitea / GitHub / GitLab, the same proxy generalizes by config.
The shared problem
Linux has no per-env-var ACL. Once a var is in a process's
environ, the process and its descendants own it. The deeper
boundary is process-level: hold the credential in a process the
agent cannot read.
Default Docker enforces that boundary 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 attempting to
read a root-owned proxy's environ gets EACCES. Escape hatches
(--cap-add=SYS_PTRACE, --cap-add=PERFMON, --privileged) are
not used by bot-bottle. Yama ptrace_scope is irrelevant — it
only relaxes the same-UID relationship check; the cross-UID
match requirement still blocks the read. On a smolmachines backend
the boundary becomes the VM line; same property, harder.
claude-code's apiKeyHelper setting 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 remaining credible designs reduce to three:
- Header-injecting reverse proxy — agent points at a localhost
URL; proxy holds the credential; proxy adds the auth header and
forwards. Cleanest fit for services that support a
BASE_URL-style override (Anthropic, OpenAI, Portkey, etc.). - Forward proxy with TLS termination — agent keeps the real
service URL; an
HTTPS_PROXYMITM intercepts, terminates TLS with a container-local CA, injects the header, re-encrypts. Heavier; required when the agent's tool can't be pointed at an explicit URL. - Don't ship the token at all — fall back to per-session login or short-lived child tokens. Operationally heavier; the long-lived OAuth token was chosen precisely because it's portable (Keychain on macOS, file on Linux).
Per-service mechanics
Anthropic / Claude Code
Today's wiring (bot_bottle/cli/start.py): the host's
BOT_BOTTLE_OAUTH_TOKEN is forwarded into the bottle as
CLAUDE_CODE_OAUTH_TOKEN via docker run -e CLAUDE_CODE_OAUTH_TOKEN
(no =value, so the value never lands on argv — good). Inside the
bottle, claude runs as node (UID 1000) with
--dangerously-skip-permissions. Its Bash tool can do
printenv CLAUDE_CODE_OAUTH_TOKEN, cat /proc/self/environ,
node -e 'console.log(process.env)' and capture the value into
the conversation. The DLP / egress story
(secret-minimization-over-dlp.md)
explains why scanning on the way out doesn't save you here.
Routing primitive: ANTHROPIC_BASE_URL is documented as a
generic proxy/gateway override, not just Bedrock/Vertex, and works
alongside bearer auth. The proxy sets
Authorization: Bearer $TOKEN and forwards to
https://api.anthropic.com. Claude as node only sees the URL,
never the token.
Confirmed gotchas:
-
SSE streaming: the proxy must not buffer responses (nginx
proxy_buffering off, or 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 to api.anthropic.com. Marked closed but verify withtcpdumporstrace -e connectagainst the pinned claude-code build before trusting the isolation. -
Tool search (
ENABLE_TOOL_SEARCH) is disabled by default whenANTHROPIC_BASE_URLis non-Anthropic; re-enable explicitly if needed. -
Out-of-band outbound traffic does not 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 + autoupdaterpypi.org,bun.sh— if the Bash tool installs Python or Bun packages during a session
A hijacked claude could exfil the captured token (or any other data) through any of these even with the proxy in place. Pair the proxy with an explicit egress allowlist for the full benefit (bot-bottle does this via pipelock).
-
Token refresh:
claude setup-tokenissues a ~1-year OAuth token with no client-side refresh, so a static proxy value is fine. The flip side is a one-year blast radius if the token leaks — seeclaude-code-token-revocation.md. -
No request signing / anti-replay on the Messages API; header rewriting is safe.
-
--baremode reads onlyANTHROPIC_API_KEY, notCLAUDE_CODE_OAUTH_TOKEN. Not relevant to the interactive flow bot-bottle ships, but worth noting if--bareis ever wired in.
Gitea (tea + git HTTPS)
Token sources, in precedence order:
GITEA_SERVER_TOKENenv var — registered viacli.EnvVars("GITEA_SERVER_TOKEN")incmd/login/add.go.~/.config/tea/config.yml(XDG) or~/.tea/tea.yml(legacy fallback) — plaintext YAML,tokenfield under the login entry.- OS credstore — OAuth logins only; PAT-based logins go to the YAML file.
There is no credential.helper analogue: no --token-file,
no FD-passing, no socket-based credential protocol. So the token
can't be hidden inside tea's process — it has to be held by a
different process the agent cannot read. For HTTPS git
operations, tea uses go-git directly with
BasicAuth{Username: token, Password: ""}
(modules/git/auth.go), bypassing git's own credential.helper
machinery. A credential-helper shim alone won't intercept
tea repo clone — the proxy has to sit on the HTTP path itself.
Header form: use Authorization: token <…>, not Bearer.
go-gitea/gitea#16734
emitted spurious "missing CSRF token" errors for Bearer on some
endpoints. The fix landed upstream, but token has always been
the header-safe choice.
No CSRF / no per-request nonce on the Gitea API for token auth, so a header-rewriting proxy is safe.
Plain git push from claude can use either the proxy
(rewritten remote URL) or a credential-helper shim that calls the
proxy. The rewritten-remote approach keeps the token bytes out of
git's credential negotiation entirely. (Note: this is parallel to
the existing git-gate in PRD 0008, which solves the SSH-push case
via a per-bottle mirror.)
GitHub / GitLab
Structurally identical to Gitea for PAT auth: stateless
Authorization: Bearer <…> (GitHub PATs and GitLab PATs both
accept Bearer, and GitHub also accepts token <…> for legacy
clients), no CSRF, no signing. Per-route allowlisting at the
proxy is the lever for narrowing blast radius. GitHub fine-grained
PATs and GitLab project-access tokens are the issuance-side
mitigation. Either composes cleanly with the same proxy.
Proxy architectures
Four shapes worth comparing. The first is the lowest-friction match for bot-bottle today.
| Shape | Pros | Cons |
|---|---|---|
| In-container reverse proxy (recommended) | Self-contained per agent, no host changes, no MITM CA, no Go-loopback workaround. Works for any service with a BASE_URL-style override (Anthropic, OpenAI, Portkey). |
Doesn't work for services that hardcode the upstream URL — requires either rewriting the client config or moving up to a forward proxy. |
| In-container forward proxy + TLS termination | Transparent to the agent's tooling — every HTTPS request gets intercepted regardless of base-URL support. | Needs a container-local CA in the trust store (same machinery PRD 0006 set up for pipelock). Has the golang/go#28866 loopback gotcha: net/http ignores HTTPS_PROXY when set to 127.0.0.1/localhost, so the proxy must bind on a non-loopback address (Docker bridge IP, host.docker.internal, or ip addr add 10.0.0.1/32 dev lo). |
| Host-side proxy | Token stays entirely outside the Linux VM. This is the Docker AI Sandbox shape. | A host daemon to maintain; the published port is reachable by any container on the host unless firewalled. UDS-across-VM doesn't work on Docker Desktop on macOS (no AF_UNIX connect() over the VM), but host.docker.internal:<port> over TCP works fine. |
| Sidecar container | Clean isolation; portable across hosts. Matches the existing pipelock / ssh-gate / git-gate topology. | Another container to orchestrate per agent; the token is in another container's env, which is a lateral move unless the sidecar runs with stricter isolation than the agent container does. |
For bot-bottle today — local Docker, per-agent containers, the root-owned-helper pattern already established by the SSH agent — the in-container reverse proxy is the lowest-friction option that gives the desired property. The sidecar-container shape is the natural evolution if the proxy needs the same per-bottle isolation that pipelock has.
Landscape of existing tools (May 2026)
Two categories:
- A. Generic LLM / API gateways that happen to support credential injection as a side feature.
- B. Purpose-built agent credential brokers — newer, closer to what bot-bottle wants.
| Tool | Category | License | Topology | Injection mechanism | ANTHROPIC_BASE_URL compatible |
Per-route allowlist | Maturity |
|---|---|---|---|---|---|---|---|
| Docker AI Sandboxes | B | Proprietary | Host-side proxy | Header overwrite, OS keychain | No (intercepts by domain) | Domain only | GA (Mar 2026) |
| Cloudflare Sandbox Auth | B | Proprietary | Sandbox sidecar + ephemeral CA | TLS intercept + Outbound Worker | No (platform-specific) | Host/IP/method | GA (Apr 2026) |
| Infisical Agent Vault | B | MIT (EE carve-out) | In-process HTTPS_PROXY forward proxy | TLS MITM, dummy-to-real swap | No — HTTPS_PROXY model | Service-level | Active; v0.19.0 May 2026, ~1k⭐ |
| nono | B | Apache-2.0 | In-process reverse proxy | Phantom token, explicit URL routing | Yes — BASE_URL=http://127.0.0.1:PORT/… |
Host + endpoint | Early alpha; v0.53.0 May 2026, 2.4k⭐ |
| Aegis | B | Apache-2.0 | In-process reverse proxy | Path routing (localhost:3100/{svc}/…) |
Configurable, undocumented for Anthropic | Method/path/rate/time | Very new, 10⭐ |
| OneCLI | B | Apache-2.0 | Reverse proxy + management UI | Host/path matching, Bitwarden integration | Configurable | Per-agent scoping | Active; v1.23.0 May 2026, 2.1k⭐ |
| Aembit | B | Proprietary | Sidecar + cloud control plane | TLS intercept, SPIFFE, JIT creds | No — intercepts by destination | Policy-based | GA (Apr 2026) |
| LiteLLM Proxy | A | MIT | Reverse proxy | Virtual key → upstream key | Yes — set base URL to LiteLLM | Route-level | 45k⭐; CVE-2026-42208 exploited Apr 2026, patch v1.83.7 |
| Portkey Gateway | A | MIT (OSS core) | Reverse proxy | Virtual key vault (cloud or Enterprise self-host) | Yes — documented for Claude Code | Config-based | Production; virtual-key vault needs Enterprise for self-host |
| Helicone | A | Apache-2.0 | Reverse proxy | Proxy header auth; agent still holds own key | Yes | No | Maintenance mode (Mintlify acq. Mar 2026) |
| LangSmith LLM Auth Proxy | A | OSS Helm | Envoy sidecar | JWT + ext_authz upstream key injection | Yes | URL allowlist | Enterprise (LangSmith ≥ v0.13.33) |
| Kong AI Gateway | A | Apache-2.0 | Reverse proxy | Plugin per-route/consumer | Yes | Plugin-level | Production, heavy |
| AWS IMDSv2 | — | n/a | Link-local | Per-instance metadata | n/a | n/a | Conceptual analog only |
Cluster commentary
-
The phantom-token pattern (nono) is the cleanest architectural fit for bot-bottle. The agent receives a per-session cryptographically random token scoped to the localhost proxy; the proxy validates and swaps for the real upstream credential. No TLS interception, no CA trust setup, works directly with
ANTHROPIC_BASE_URL. Blocker: nono is explicitly "early alpha, not security audited." -
TLS-MITM forward proxies (Infisical Agent Vault, Cloudflare Sandbox Auth, Aembit, the existing pipelock) all double up on the CA-trust machinery PRD 0006 already built for pipelock. Adopting Agent Vault would mean two MITM proxies in each bottle unless one is dropped. Also subject to
golang/go#28866— must bind on a non-loopback address. -
LLM gateways (LiteLLM, Portkey, Helicone, Kong) all support credential injection but are built for cost / observability / fallback, not isolation. Specific concern: the LiteLLM CVE-2026-42208 (CVSS 9.3, pre-auth SQL injection on the Bearer auth path, exploited within 36 hours of disclosure) is a reminder that any self-hosted DB-backed credential gateway is itself a high-value attack target. Prefer a flat-file or env-only credential store on the sidecar over a database.
-
Helicone is in maintenance mode since the Mintlify acquisition in March 2026 (security fixes only, no features). Treat as legacy.
-
Portkey's virtual-key vault — the actual credential-injection feature — requires the Enterprise plan for self-host. The open-source gateway alone does routing without injection.
Build-vs-adopt synthesis
Architecturally aligned: nono. Phantom-token + explicit-URL routing matches the design recommended here exactly; zero TLS work. But "not security audited" + "early alpha" means adopting it is a bet on the project rather than a buy-vs-build win.
Most mature OSS purpose-built: Infisical Agent Vault. MIT, v0.19.0 active, v0.17.0 added a containerized agent mode that maps directly to bot-bottle. Friction is the TLS-MITM topology — another container-local CA, the Go-loopback workaround, duplication with pipelock's existing TLS interception layer.
For the immediate Anthropic-token slice, a ~100-line Rust or Go reverse proxy modeled on nono's phantom-token shape is probably less work and less risk than adopting either. The surface is small: hold the token, inject one header, forward to api.anthropic.com over TLS, pass through SSE without buffering. For Gitea / GitHub / GitLab the same proxy generalizes by config.
The build path also keeps the credential store flat (env file or mode-600 YAML on the sidecar), which sidesteps the "DB-backed-gateway as attack surface" concern the LiteLLM CVE exposed.
Recommended path forward
In priority order:
-
In-container reverse proxy holding
CLAUDE_CODE_OAUTH_TOKEN. Highest-leverage change: credential isolation and the ability to drop theapi.anthropic.comTLS passthrough in pipelock (seesecret-minimization-over-dlp.md§2). Proxy runs as root inside the agent container, listens on127.0.0.1(no Go-loopback issue for the reverse-proxy case — the agent isn't usingHTTPS_PROXY), injectsAuthorization: Bearer …, sets the bottle'sANTHROPIC_BASE_URLto the local URL. -
Layer in
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1plus the existing pipelock egress allowlist (api.anthropic.com only, plus the per-agent set of MCP / git / package-registry hosts). A hijacked claude can no longer exfil through statsig / Sentry / npm even if it captures something. Also disable Sentry error reporting viaDISABLE_ERROR_REPORTING=1. -
Generalize the same proxy to forge tokens. Add a manifest field along the lines of
forge: { kind: "gitea", url, tokenRef }so a per-bottle token reference resolves at launch, the proxy starts as root beforenodeis exec'd, andteaplus git HTTPS remotes are pre-configured to point at the proxy. UseAuthorization: token <…>for Gitea,Bearerfor GitHub / GitLab. -
Scope-narrow the tokens at issuance.
repo:writeonly, noadmin, no user management. Fine-grained GitHub PATs, GitLab project-access tokens, Gitea per-repo tokens. Cheapest single thing to do; bounds blast radius regardless of whether the proxy ships. -
Allowlist at the proxy once usage is stable. Method + path filter keyed off the agent's actual API calls; reject everything else. Doesn't prevent abuse within the allowlist but narrows the surface to known good operations.
The current docker run -e CLAUDE_CODE_OAUTH_TOKEN pattern is
fine for argv hygiene on the host, but inside the bottle the
token is fully exposed. The proxy pattern moves it across a
kernel-enforced boundary — the same property the SSH agent
already gives us for keys, and the same property the git-gate
already gives us for upstream push credentials.
Sources
Mechanics
- Authentication — Claude Code docs
- LLM gateway configuration — Claude Code docs
- Claude Code environment variables
- GitHub issue anthropics/claude-code#36998 — interactive mode bypasses ANTHROPIC_BASE_URL
- GitHub issue anthropics/claude-code#11587 — apiKeyHelper vs CLAUDE_CODE_OAUTH_TOKEN
proc_pid_environ(5)man page- Documenting ptrace access mode checking — LWN
- StepSecurity — Claude Code Action outbound network analysis
- Manage API key environment variables — Claude Help Center
- tea source —
cmd/login/add.go - tea source —
modules/config/config.go - tea source —
modules/git/auth.go - Gitea API usage docs
- go-gitea/gitea#16734 —
Authorization: Bearertriggers spurious CSRF - golang/go#28866 —
net/httpignoresHTTPS_PROXYfor127.0.0.1/localhost - git credential helper docs
Landscape
- Docker AI Sandboxes — credentials
- docker/desktop-feedback#130 — custom injection rules
- Cloudflare Sandbox Auth blog
- Cloudflare Outbound Workers GA changelog
- Cloudflare Sandboxes GA — InfoQ
- Infisical agent-vault — GitHub
- Infisical agent-vault — releases
- Infisical agent-vault — blog
- nono — GitHub
- nono — phantom token blog
- Aegis — GitHub
- OneCLI — GitHub
- Sandbox0 — GitHub
- Buildkite Cleanroom — GitHub
- Aembit IAM for Agentic AI — GA
- Aembit Claude integration docs
- LiteLLM CVE-2026-42208 — Sysdig writeup
- LiteLLM — GitHub
- Portkey + Claude Code
- Portkey gateway — GitHub
- Helicone maintenance mode announcement
- LangSmith LLM auth proxy docs
- AWS IMDSv2 docs
- Pipelock — Help Net Security
- CB4A IETF draft — Credential Broker for Agents
- List of coding agent sandboxes (May 2026)