# 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`](secret-minimization-over-dlp.md) (the architectural framing — why this matters), and to [`local-vs-remote-agent-execution.md`](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//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_PROXY` MITM 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_CLAUDE_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`](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`, and `X-Claude-Code-Session-Id` untouched.** Stripping them breaks tool use / extended thinking / session aggregation. - **GitHub issue [#36998](https://github.com/anthropics/claude-code/issues/36998)**: interactive mode historically bypassed `ANTHROPIC_BASE_URL` for some startup calls (auth validation / org lookup), connecting directly to api.anthropic.com. Marked closed but verify with `tcpdump` or `strace -e connect` against the pinned claude-code build before trusting the isolation. - **Tool search** (`ENABLE_TOOL_SEARCH`) is disabled by default when `ANTHROPIC_BASE_URL` is 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 + autoupdater - `pypi.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-token` issues 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 — see [`claude-code-token-revocation.md`](claude-code-token-revocation.md). - **No request signing / anti-replay** on the Messages API; header rewriting is safe. - **`--bare` mode** reads only `ANTHROPIC_API_KEY`, not `CLAUDE_CODE_OAUTH_TOKEN`. Not relevant to the interactive flow bot-bottle ships, but worth noting if `--bare` is ever wired in. ### Gitea (`tea` + git HTTPS) **Token sources, in precedence order:** 1. **`GITEA_SERVER_TOKEN` env var** — registered via `cli.EnvVars("GITEA_SERVER_TOKEN")` in `cmd/login/add.go`. 2. **`~/.config/tea/config.yml`** (XDG) or `~/.tea/tea.yml` (legacy fallback) — plaintext YAML, `token` field under the login entry. 3. **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](https://github.com/go-gitea/gitea/issues/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:` 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: 1. **In-container reverse proxy holding `CLAUDE_CODE_OAUTH_TOKEN`.** Highest-leverage change: credential isolation **and** the ability to drop the `api.anthropic.com` TLS passthrough in pipelock (see [`secret-minimization-over-dlp.md`](secret-minimization-over-dlp.md) §2). Proxy runs as root inside the agent container, listens on `127.0.0.1` (no Go-loopback issue for the reverse-proxy case — the agent isn't using `HTTPS_PROXY`), injects `Authorization: Bearer …`, sets the bottle's `ANTHROPIC_BASE_URL` to the local URL. 2. **Layer in `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`** plus 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 via `DISABLE_ERROR_REPORTING=1`. 3. **Generalize the same proxy to Git-host tokens.** Add a manifest field along the lines of `git_host: { kind: "gitea", url, tokenRef }` so a per-bottle token reference resolves at launch, the proxy starts as root before `node` is exec'd, and `tea` plus git HTTPS remotes are pre-configured to point at the proxy. Use `Authorization: token <…>` for Gitea, `Bearer` for GitHub / GitLab. 4. **Scope-narrow the tokens at issuance.** `repo:write` only, no `admin`, 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. 5. **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](https://code.claude.com/docs/en/authentication) - [LLM gateway configuration — Claude Code docs](https://code.claude.com/docs/en/llm-gateway) - [Claude Code environment variables](https://code.claude.com/docs/en/env-vars) - [GitHub issue anthropics/claude-code#36998 — interactive mode bypasses ANTHROPIC_BASE_URL](https://github.com/anthropics/claude-code/issues/36998) - [GitHub issue anthropics/claude-code#11587 — apiKeyHelper vs CLAUDE_CODE_OAUTH_TOKEN](https://github.com/anthropics/claude-code/issues/11587) - [`proc_pid_environ(5)` man page](https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html) - [Documenting ptrace access mode checking — LWN](https://lwn.net/Articles/692203/) - [StepSecurity — Claude Code Action outbound network analysis](https://www.stepsecurity.io/blog/anthropics-claude-code-action-security-how-to-secure-claude-code-in-github-actions-with-harden-runner) - [Manage API key environment variables — Claude Help Center](https://support.claude.com/en/articles/12304248-manage-api-key-environment-variables-in-claude-code) - [tea source — `cmd/login/add.go`](https://gitea.com/gitea/tea/src/branch/main/cmd/login/add.go) - [tea source — `modules/config/config.go`](https://gitea.com/gitea/tea/src/branch/main/modules/config/config.go) - [tea source — `modules/git/auth.go`](https://gitea.com/gitea/tea/src/branch/main/modules/git/auth.go) - [Gitea API usage docs](https://docs.gitea.com/development/api-usage) - [go-gitea/gitea#16734 — `Authorization: Bearer` triggers spurious CSRF](https://github.com/go-gitea/gitea/issues/16734) - [golang/go#28866 — `net/http` ignores `HTTPS_PROXY` for `127.0.0.1`/`localhost`](https://github.com/golang/go/issues/28866) - [git credential helper docs](https://git-scm.com/docs/gitcredentials) ### Landscape - [Docker AI Sandboxes — credentials](https://docs.docker.com/ai/sandboxes/security/credentials/) - [docker/desktop-feedback#130 — custom injection rules](https://github.com/docker/desktop-feedback/issues/130) - [Cloudflare Sandbox Auth blog](https://blog.cloudflare.com/sandbox-auth/) - [Cloudflare Outbound Workers GA changelog](https://developers.cloudflare.com/changelog/post/2026-04-13-sandbox-outbound-workers-tls-auth/) - [Cloudflare Sandboxes GA — InfoQ](https://www.infoq.com/news/2026/04/cloudflare-sandboxes-ga/) - [Infisical agent-vault — GitHub](https://github.com/Infisical/agent-vault) - [Infisical agent-vault — releases](https://github.com/Infisical/agent-vault/releases) - [Infisical agent-vault — blog](https://infisical.com/blog/agent-vault-the-open-source-credential-proxy-and-vault-for-agents) - [nono — GitHub](https://github.com/always-further/nono) - [nono — phantom token blog](https://nono.sh/blog/blog-credential-injection) - [Aegis — GitHub](https://github.com/getaegis/aegis) - [OneCLI — GitHub](https://github.com/onecli/onecli) - [Sandbox0 — GitHub](https://github.com/sandbox0-ai/sandbox0) - [Buildkite Cleanroom — GitHub](https://github.com/buildkite/cleanroom) - [Aembit IAM for Agentic AI — GA](https://aembit.io/blog/aembit-iam-for-agentic-ai-is-now-generally-available/) - [Aembit Claude integration docs](https://docs.aembit.io/user-guide/access-policies/server-workloads/guides/claude) - [LiteLLM CVE-2026-42208 — Sysdig writeup](https://www.sysdig.com/blog/cve-2026-42208-targeted-sql-injection-against-litellms-authentication-path-discovered-36-hours-following-vulnerability-disclosure/) - [LiteLLM — GitHub](https://github.com/BerriAI/litellm) - [Portkey + Claude Code](https://portkey.ai/docs/virtual_key_old/integrations/libraries/claude-code) - [Portkey gateway — GitHub](https://github.com/Portkey-ai/gateway) - [Helicone maintenance mode announcement](https://dev.to/torrixai/helicone-is-now-in-maintenance-mode-here-is-how-to-switch-to-a-self-hosted-alternative-in-5-4li0) - [LangSmith LLM auth proxy docs](https://docs.langchain.com/langsmith/llm-auth-proxy-self-hosted) - [AWS IMDSv2 docs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) - [Pipelock — Help Net Security](https://www.helpnetsecurity.com/2026/05/04/pipelock-open-source-ai-agent-firewall/) - [CB4A IETF draft — Credential Broker for Agents](https://www.ietf.org/archive/id/draft-hartman-credential-broker-4-agents-00.html) - [List of coding agent sandboxes (May 2026)](https://gist.github.com/wincent/2752d8d97727577050c043e4ff9e386e)