# PRD 0010: Credential proxy for agent-bound API tokens

- **Status:** Draft
- **Author:** didericis
- **Created:** 2026-05-13

## Summary

Per-bottle sidecar container that holds API tokens (Anthropic
OAuth, GitHub PAT, Gitea PAT, npm token). The agent container
keeps only URLs in its environ; the sidecar injects the right
`Authorization` header and forwards over TLS to the upstream. The
boundary is the container line — PID, mount, and network
namespaces separate the agent's container from the sidecar's, so
from inside the agent the sidecar's processes are not visible in
`/proc`, cannot be `ptrace`'d, and share no memory. Reaching the
sidecar's environ requires escaping the agent container — the same
threshold pipelock and git-gate already rely on.

AWS / SigV4 is explicitly out of scope — it is per-request signing,
not header injection, and does not fit this proxy's shape. If a
bottle needs AWS credentials later, that lives in a separate PRD.

## Problem

Today `CLAUDE_CODE_OAUTH_TOKEN` (and any `bottle.env` secrets such
as a Gitea PAT, GitHub PAT, or npm token) gets `docker run -e`'d
straight into the agent's environ. Inside the bottle the agent
runs as `node` with `--dangerously-skip-permissions`; its Bash
tool can do `printenv`, `cat /proc/self/environ`, or
`node -e 'console.log(process.env)'` and capture every value into
the conversation. From there a prompt-injected or hijacked agent
can exfil over any allowed egress (api.anthropic.com itself if
nothing else).

Linux has no per-env-var ACL — once a variable is in a process's
environ, the process and its descendants own it. The credible
boundary is container-level: hold the credential in a separate
container the agent cannot reach. Default Docker's namespace
isolation enforces that — the same property pipelock and git-gate
already rely on.

The research note
[`agent-credential-proxy-landscape.md`](../research/agent-credential-proxy-landscape.md)
surveys the existing tools and concludes that a small
claude-bottle-specific reverse proxy is less work and less risk
than either adopting nono (alpha, unaudited) or Infisical Agent
Vault (TLS-MITM topology that doubles up on pipelock's CA stack).
This PRD is the build.

## Goals / Success Criteria

Each test runs inside a bottle whose manifest declares the four
common upstreams (Anthropic, GitHub, Gitea, npm) as
`bottle.cred_proxy.routes` entries:

1. **No plaintext tokens in the agent's environ.** `printenv` and
   `cat /proc/self/environ` from the agent's shell return only
   URLs pointing at `cred-proxy:<PORT>/...`. None of the
   `cred_proxy.routes[].token_ref` host env-var values appear.
2. **Container boundary holds.** From the agent's shell, `ps aux`
   does not list the cred-proxy process; there is no `/proc/<X>`
   entry for it to read. The sidecar's hostname (`cred-proxy`)
   resolves only on the bottle's internal network — from a
   different bottle or from the host, the name does not resolve.
3. **Anthropic API works.** `claude` makes a successful streaming
   tool-use round-trip via `ANTHROPIC_BASE_URL` →
   `cred-proxy:<PORT>/anthropic`. SSE chunks arrive without
   buffering; `anthropic-version`, `anthropic-beta`, and
   `X-Claude-Code-Session-Id` headers round-trip untouched.
4. **`tea` / REST API against declared upstreams works.**
   `tea pr list` against a route's upstream succeeds; the
   upstream sees the proxy-injected token, not the agent's.
   `git push` is *not* on the cred-proxy path — that goes
   through `bottle.git` / git-gate (where gitleaks runs).
5. **npm install works.** `npm install <public-package>`
   succeeds against the registry pointed at the proxy. A scoped
   install that requires the token (e.g. against a private
   registry) also succeeds.
6. **Wrong token rejected at the source, not silently swapped.**
   If the agent tries to send its own `Authorization: …` header,
   the proxy strips and replaces with the configured one. A
   manifest token revoked at the upstream produces a 401 to the
   agent, not a 5xx. Git smart-HTTP push paths
   (`/git-receive-pack`, `/info/refs?service=git-receive-pack`)
   return 403 unconditionally — push must go through git-gate's
   gitleaks-scanned SSH path.

## Non-goals

- **AWS / SigV4.** Per-request signing is a different shape; a
  bearer-injecting proxy doesn't help. Hold for a future PRD
  (likely an IMDS emulator sidecar handing out short-lived STS
  credentials).
- **DB-backed credential store.** Flat env / mode-600 file only.
  The LiteLLM CVE-2026-42208 incident is the cautionary tale:
  any DB-backed credential gateway is itself a high-value attack
  target.
- **Generic LLM-gateway features.** No cost tracking, no
  fallbacks, no virtual keys, no multi-tenant routing, no usage
  metering. The proxy is a credential-injection trust endpoint,
  not a gateway.
- **Subsuming pipelock.** pipelock keeps its egress-allowlist
  role. It drops the `api.anthropic.com` TLS-MITM job because
  cred-proxy is now the trust endpoint for that host; everything
  else pipelock does stays.
- **TLS interception inside the bottle.** The agent talks plain
  HTTP to loopback; cred-proxy speaks real HTTPS outbound. No
  container-local CA, no `golang/go#28866` loopback workaround.
- **Cross-bottle credential sharing.** One proxy per bottle, same
  one-sidecar-per-agent posture as pipelock and git-gate.
- **`claude --bare` mode.** Reads only `ANTHROPIC_API_KEY`, not
  the OAuth token. Not in claude-bottle's flow today.
- **MCP-server tokens, package-installer tokens for languages
  beyond npm.** PyPI / Bun / cargo can land in a follow-up if
  needed; the routing pattern generalizes.

## Scope

### In scope

- **Manifest field.** `bottle.cred_proxy.routes: [Route, ...]`.
  Each route carries `path` (agent-facing prefix), `upstream`
  (HTTPS upstream URL), `auth_scheme` (`Bearer` or `token`),
  `token_ref` (name of a host env var the CLI resolves at launch
  time), and an optional `role` (string or list of strings — see
  "Agent-side rewrites" below). Routes are independent — there is
  no `Kind` enum or per-kind hardcoded path/upstream mapping; the
  manifest is the source of truth for the proxy's runtime route
  table.
- **cred-proxy sidecar.** Runs as its own container on the
  bottle's internal docker network with hostname `cred-proxy`,
  listening on `0.0.0.0:<PORT>` bound to the internal interface.
  No host port published. Holds the tokens in the sidecar
  container's environ — never on argv, never written to disk.
  Per-route handler: inject the configured header, forward over
  TLS, stream the response back without buffering.
- **Agent-side rewrites.** A route's `role` (string or list of
  strings) drives optional agent-side dotfile/env writes when the
  sidecar comes up. Known roles:
  - `anthropic-base-url` (singleton): sets
    `ANTHROPIC_BASE_URL=http://cred-proxy:<PORT><route.path>` in
    the agent's environ. Used for the Anthropic OAuth path.
  - `npm-registry` (singleton): writes
    `registry=http://cred-proxy:<PORT><route.path>` to `~/.npmrc`.
  - `git-insteadof`: writes a `[url "http://cred-proxy:<PORT><route.path>"]
    insteadOf = <route.upstream>/` block to `~/.gitconfig`.
    Suppressed when `bottle.git` already brokers the same host:
    git-gate is the canonical git path there — its pre-receive
    runs gitleaks before forwarding pushes; a cred-proxy
    `https://<host>/` rewrite would route HTTPS git ops around
    the gate. (cred-proxy independently refuses smart-HTTP push
    paths at runtime — see "Smart-HTTP push refused" below — but
    suppressing the rewrite means `git clone https://<host>/...`
    doesn't have a tempting shortcut that just confuses later.)
  - `tea-login`: adds a `logins:` entry to
    `~/.config/tea/config.yml` pointing at the proxy. Used for
    Gitea instances; combine with `git-insteadof` for full agent
    coverage.

  Routes without a `role` are pure proxy entries — the proxy
  handles them at runtime, but no agent-side rewrite happens. The
  singleton roles must appear on at most one route per bottle
  (manifest validation enforces this).
- **Sidecar lifecycle.** Mirrors `DockerGitGate` /
  `DockerPipelockProxy` in shape: `prepare` is host-side and
  side-effect-free; `start` does `docker create` + `docker start`
  on the bottle's internal network with hostname `cred-proxy`;
  `stop` is idempotent `docker rm -f`. Container name:
  `claude-bottle-cred-proxy-<slug>`. The agent container starts
  after the sidecar is up so DNS resolution succeeds on the
  agent's first call.
- **pipelock interop.** cred-proxy's outbound HTTPS traverses
  pipelock: the sidecar's environ sets `HTTPS_PROXY` /
  `HTTP_PROXY` to the per-bottle pipelock URL, and the cred-proxy
  image's entrypoint runs `update-ca-certificates` over the
  per-bottle pipelock CA (`docker cp`'d into
  `/usr/local/share/ca-certificates/pipelock.crt` before start)
  so cred-proxy's HTTPS client trusts pipelock's bumped certs.
  Pipelock's allowlist + body scanner therefore apply to
  cred-proxy → upstream the same way they apply to direct agent
  traffic. Only `api.anthropic.com` stays on
  `passthrough_domains` (its bodies are LLM conversation text
  that legitimately trips DLP heuristics); github / gitea / npm
  hosts are auto-added to the allowlist (so cred-proxy can reach
  them) but NOT to passthrough, so pipelock body-scans them.
- **Smart-HTTP push refused.** cred-proxy returns 403 for paths
  matching `/info/refs?service=git-receive-pack` and any path
  ending in `/git-receive-pack`. Fetch (upload-pack) is allowed.
  Push must go through `bottle.git` / git-gate, where the
  gitleaks pre-receive hook runs. This holds even when no
  matching `bottle.git` entry exists — the proxy is not a
  scanned-push path, period.
- **Plan rendering.** `bottle_plan.py` and the y/N preflight
  show: which tokens are configured (kind + ref name, not the
  value), the proxy port, the routes the proxy will publish.
- **Drop the existing `CLAUDE_CODE_OAUTH_TOKEN` forward in
  `prepare.py`.** Today it lands in the agent's environ; once
  this PRD ships, it lands in the cred-proxy sidecar's environ
  instead.
- **Tests.** Integration tests for each of the six success
  criteria; unit tests for manifest parsing, route table
  generation, header injection.

### Out of scope

- AWS / SigV4 (see Non-goals).
- Per-method / per-path allowlist *inside* a kind. Defer to a
  follow-up once observed traffic stabilizes.
- Replacing `bottle.env` for non-token secrets. The proxy
  handles the four kinds listed above; other env vars keep their
  current path.
- Migrating an in-flight bottle from "token in agent env" to
  "token via proxy" mid-session. Restart required.
- Audit logging. The proxy doesn't write request logs in v1.
  Add only if a concrete debugging need surfaces.

## Proposed Design

### Architecture

```
┌── Host (macOS) ──────────────────────────────────────────────────┐
│   Secrets at rest (keychain / .env):                             │
│     CLAUDE_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN,                     │
│     GITEA_SERVER_TOKEN, NPM_TOKEN                                │
│        │ docker run -e KEY  (no =VALUE on argv)                  │
│        ▼                                                         │
│   ┌── per-bottle internal docker network ──────────────────────┐ │
│   │                                                            │ │
│   │   ┌── agent container ─────────────────────────────────┐   │ │
│   │   │  claude as node (UID 1000)                         │   │ │
│   │   │  --dangerously-skip-permissions                    │   │ │
│   │   │  environ: URLs only, no plaintext tokens           │   │ │
│   │   │    ANTHROPIC_BASE_URL=http://cred-proxy:PORT/an..  │   │ │
│   │   │    npm  registry     → http://cred-proxy:PORT/npm/ │   │ │
│   │   │    git  insteadOf    → http://cred-proxy:PORT/...  │   │ │
│   │   │    tea  --url        → http://cred-proxy:PORT/gite │   │ │
│   │   └────────────┬───────────────────────────────────────┘   │ │
│   │                │ HTTP, DNS → cred-proxy                    │ │
│   │                ▼                                           │ │
│   │   ┌── cred-proxy sidecar ──────────────────────────────┐   │ │
│   │   │  distroless image, no shell, runs as root          │   │ │
│   │   │  hostname: cred-proxy   listens 0.0.0.0:PORT       │   │ │
│   │   │  tokens live ONLY in this container's environ      │   │ │
│   │   │    /anthropic → api.anthropic.com   Bearer         │   │ │
│   │   │    /gh-api    → api.github.com      Bearer         │   │ │
│   │   │    /gh-git    → github.com          Bearer         │   │ │
│   │   │    /gitea     → gitea.dideric.is    token          │   │ │
│   │   │    /npm       → registry.npmjs.org  Bearer         │   │ │
│   │   │  SSE pass-through, no buffering                    │   │ │
│   │   └────────────┬───────────────────────────────────────┘   │ │
│   │                │ HTTPS                                     │ │
│   │                ▼                                           │ │
│   │   ┌── pipelock sidecar (egress allowlist) ─────────────┐   │ │
│   │   │  allow: api.anthropic.com, api.github.com,         │   │ │
│   │   │         github.com, gitea.dideric.is,              │   │ │
│   │   │         registry.npmjs.org                         │   │ │
│   │   │  block: statsig, sentry, autoupdater, *            │   │ │
│   │   └────────────┬───────────────────────────────────────┘   │ │
│   └────────────────┼───────────────────────────────────────────┘ │
│                    ▼                                             │
└────────────────────┼─────────────────────────────────────────────┘
                     ▼
              Upstream APIs


Why the agent can't reach the sidecar's environ:
   ┌───────────────────────────────────────────────────────────────┐
   │  Different container = different PID, mount, and network ns.  │
   │  The agent's /proc shows only the agent's own processes;      │
   │  the cred-proxy PID is not visible — no /proc/<X>/environ     │
   │  to read, no PID to ptrace, no shared memory.                 │
   │                                                               │
   │  Reaching the sidecar's environ requires escaping the agent   │
   │  container — the same threshold pipelock and git-gate rely    │
   │  on. Default Docker isolation is the boundary.                │
   └───────────────────────────────────────────────────────────────┘
```

### New components

- **`claude_bottle/cred_proxy.py`** (new): abstract `CredProxy`
  + `CredProxyPlan` dataclass. `prepare` is host-side and
  side-effect-free; renders the route table and resolves
  `TokenRef`s against host env. Mirrors the existing `GitGate` /
  `Pipelock` shape.
- **`claude_bottle/backend/docker/cred_proxy.py`** (new):
  `DockerCredProxy` concrete subclass. `start` does
  `docker create` on the bottle's internal network with hostname
  `cred-proxy`, copies the route-table file into the container,
  then `docker start`. `stop` is idempotent `docker rm -f`.
  Container name: `claude-bottle-cred-proxy-<slug>`.
- **`claude_bottle/backend/docker/provision/cred_proxy.py`**
  (new): renders `ANTHROPIC_BASE_URL`, `~/.npmrc`,
  `~/.gitconfig` `insteadOf` blocks, and `~/.config/tea/config.yml`
  into the agent's home for each declared kind — all pointing at
  `http://cred-proxy:<PORT>/...`.
- **cred-proxy image.** Minimal base + the proxy binary, no
  shell. Pinned by digest, baked at build time. Footprint sized
  to match git-gate's image rather than the full agent image.

### Existing code touched

- **`claude_bottle/manifest.py`** — add `CredProxyRoute`,
  `CredProxyConfig`, `Bottle.cred_proxy: CredProxyConfig`. Parse
  + validate route shape, role enum, path uniqueness, singleton-
  role constraints.
- **`claude_bottle/backend/docker/prepare.py`** — drop the
  legacy `CLAUDE_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN`
  forward entirely. cred-proxy is the only path the Anthropic
  OAuth token reaches the bottle. When a route claims the
  `anthropic-base-url` role, write `ANTHROPIC_BASE_URL`
  (pointing at the proxy) plus a non-secret placeholder for
  `CLAUDE_CODE_OAUTH_TOKEN` (claude-code refuses to start
  otherwise; the proxy strips & replaces on every request).
  Bottles that need claude-code to authenticate must declare
  the route; there is no fallback.
- **`claude_bottle/backend/docker/backend.py`** — instantiate
  `DockerCredProxy` alongside `DockerPipelockProxy` and
  `DockerGitGate`; thread its `prepare` / `start` / `stop`
  through `resolve_plan` / `launch`.
- **`claude_bottle/backend/docker/launch.py`** — add cred-proxy
  start/stop to the `ExitStack` after pipelock and before the
  agent; populate `pipelock_proxy_url` + `pipelock_ca_host_path`
  on the cred-proxy plan so its outbound HTTPS routes through
  pipelock.
- **`claude_bottle/backend/docker/bottle_plan.py`** — new
  `cred_proxy_plan` field; preflight shows route count + token
  refs + a path→upstream line per route; `to_dict` emits a
  `cred_proxy` array of `{path, upstream, auth_scheme, token_ref,
  roles}`.
- **`claude_bottle/pipelock.py`** — `pipelock_token_hosts` derives
  from each route's `UpstreamHost` (not a hardcoded Kind→hosts
  map). Allowlist auto-includes them; passthrough does not (the
  proxy trusts pipelock's CA so MITM works).
- **`README.md`** — architecture diagram includes the cred-proxy
  lane; manifest section documents `bottle.cred_proxy.routes`.
- **`claude-bottle.example.json`** — one bottle demonstrates the
  four common routes (Anthropic, GitHub, Gitea, npm).
- **Tests** — manifest parsing/validation, route lift + token-env
  slot assignment, role-based dispatch in the provisioner,
  pipelock allowlist derivation from routes. Integration test
  exercises header inject + smart-HTTP push refusal.

### Data model changes

```python
@dataclass(frozen=True)
class CredProxyRoute:
    Path: str                       # "/anthropic/" — must start and end with /
    Upstream: str                   # "https://api.anthropic.com" — https only
    AuthScheme: str                 # "Bearer" or "token"
    TokenRef: str                   # name of host env var
    Role: tuple[str, ...] = ()      # provisioner tags; see CRED_PROXY_ROLES
    UpstreamHost: str = ""          # derived from Upstream

@dataclass(frozen=True)
class CredProxyConfig:
    routes: tuple[CredProxyRoute, ...] = ()

@dataclass(frozen=True)
class Bottle:
    ...
    cred_proxy: CredProxyConfig = field(default_factory=CredProxyConfig)
```

Validation:

- `Path` non-empty, starts and ends with `/`; unique across all
  routes in a bottle (the proxy routes by longest-prefix match).
- `Upstream` is `https://...` with a non-empty host.
- `AuthScheme` is one of `Bearer`, `token`.
- `TokenRef` non-empty; its value is resolved against
  `os.environ` at launch (fail fast with a clear "host env var X
  is unset" if missing).
- `Role` items are one of `anthropic-base-url`, `npm-registry`,
  `git-insteadof`, `tea-login`. Single string accepted as sugar
  for a one-item list.
- Singleton roles (`anthropic-base-url`, `npm-registry`) appear
  on at most one route per bottle.
- A route MAY name the same host as a `bottle.git` entry. The
  two paths broker different protocols — git-gate holds an SSH
  `IdentityFile` for push/fetch and runs gitleaks; cred-proxy
  holds a PAT for HTTPS REST API calls (`tea`, `gh`, octokit).
  The common dev setup uses both on the same host. The
  provisioner's `git-insteadof` role is suppressed in that case
  (see Agent-side rewrites).

### Example routes

| Common upstream        | Route                                                                                                                               |
|------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| Anthropic API          | `{path: "/anthropic/", upstream: "https://api.anthropic.com", auth_scheme: "Bearer", token_ref: "…", role: "anthropic-base-url"}`   |
| GitHub REST API        | `{path: "/gh-api/", upstream: "https://api.github.com", auth_scheme: "Bearer", token_ref: "…"}`                                     |
| GitHub git transport   | `{path: "/gh-git/", upstream: "https://github.com", auth_scheme: "Bearer", token_ref: "…", role: "git-insteadof"}`                  |
| Gitea instance         | `{path: "/gitea/<host>/", upstream: "https://<host>", auth_scheme: "token", token_ref: "…", role: ["git-insteadof", "tea-login"]}` |
| npm registry           | `{path: "/npm/", upstream: "https://registry.npmjs.org", auth_scheme: "Bearer", token_ref: "…", role: "npm-registry"}`              |

Gitea uses `Authorization: token` rather than `Bearer` to
sidestep `go-gitea/gitea#16734`. The proxy strips any incoming
`Authorization` header before injecting its own — the agent
cannot smuggle a stolen token through this path.

### External dependencies

The proxy binary. Two real options:

- **Python (stdlib)** — `http.server` + `urllib`/`http.client`,
  no new pip packages. Matches CLAUDE.md's "bash-first, low-deps"
  posture. SSE pass-through is fiddly but doable.
- **Go single binary** — cleaner SSE story, smaller runtime,
  one static binary in a scratch/distroless image. New build
  dependency.

Default: Python in a minimal `python:3.X-slim` image (or alpine
if we want smaller). Reconsider in the implementation PR if SSE
behavior is troublesome under load.

No new Python packages. No DB. No admin API. The proxy's
configuration is a single mode-600 JSON file copied into the
sidecar at `docker create` time and read by the proxy at startup
from `/run/cred-proxy/routes.json`.

## Future work

- **AWS / SigV4.** Likely an IMDS emulator sidecar handing out
  short-lived STS tokens. Different threat model (the agent
  ends up holding the STS creds — the proxy just shortens
  their lifetime). Separate PRD.
- **Per-method / per-path allowlist** inside a kind. Once the
  set of API operations claude actually performs is observed,
  reject everything else. Narrows the within-allowlist surface.
- **Short-lived token minting.** For services that support it
  (GitHub Apps, GitLab project-access tokens, fine-grained
  PATs with TTL), have the proxy mint a fresh per-session
  child credential from a long-lived parent.
- **Smolmachines colocation.** Same packing question as
  pipelock / git-gate; under a future microVM backend the
  cred-proxy could share a VM with the agent (today's per-bottle
  network gives it its own container, not its own VM) or sit in
  its own VM (stricter isolation, an extra TCP hop). Backend
  decision, not a manifest decision.
- **More kinds.** PyPI, Bun, cargo, Docker Hub. The routing
  pattern generalizes; add as needed.

## Considered alternatives

### In-container proxy (root inside the agent container)

Run cred-proxy as PID 1 of the agent container, listening on
`127.0.0.1:<PORT>`, with claude exec'd as `node` (UID 1000) only
after the proxy is bound. The boundary in that shape is the
kernel's cross-UID `ptrace_may_access` check — `node` cannot read
root's `/proc/<pid>/environ` and cannot `ptrace` attach.

Pros: one less container per bottle; slightly faster bottle
startup; no extra docker create/start/stop dance.

Rejected because:

- **Weaker isolation.** The boundary collapses to UID separation
  alone. Any container-root compromise inside the agent (setuid
  bug in the image, accidentally mounted docker socket, a kernel
  CVE, accidental `--privileged`) reads the proxy's environ via
  `/proc/<pid>/environ`. The sidecar's namespace separation
  cannot be bypassed from inside the agent container without a
  container escape.
- **Inconsistent with the existing topology.** pipelock and
  git-gate are already sidecars on the bottle's internal network.
  cred-proxy slots into the same shape and reuses the same
  lifecycle abstractions (`BottleBackend.prepare/start/stop`,
  `ExitStack` ordering, plan rendering).
- **Coupled to the agent image.** The proxy binary, its
  entrypoint, and its priv-drop logic would all live in the
  agent's Dockerfile. A sidecar image evolves independently —
  agents can change base, language, or tooling without touching
  the proxy.
- **PID-1 babysitting.** The "proxy supervises, then `exec
  setpriv → node`" entrypoint introduces a class of issues
  (zombie reaping, signal forwarding, exit-code propagation) that
  the sidecar shape avoids.

## Open questions

- **~~Field name.~~** Resolved during iteration: routes live at
  `bottle.cred_proxy.routes` (the nested object reserves room for
  per-bottle proxy settings later). Each route is independent;
  no `Kind` enum on the route. A `role` field drives the
  optional agent-side rewrites — see "Agent-side rewrites" in
  Scope.
- **Python vs Go for the proxy.** Default: Python, revisit
  during implementation if SSE pass-through is unreliable.
- **Sidecar image base.** Distroless (smallest, no shell — hardest
  to debug), Python slim (debuggable, larger), or scratch + a
  statically-linked Go binary (smallest if Go). Default: whatever
  fits the chosen language with the smallest non-shell base;
  revisit if debuggability bites during implementation.
- **Belt-and-braces on outbound telemetry.** Set
  `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` and
  `DISABLE_ERROR_REPORTING=1` in the agent's environ by
  default? Default: yes — they don't route through
  `ANTHROPIC_BASE_URL`, so the proxy doesn't catch them; the
  flags are the only off switch.
- **`git push` over a rewritten URL vs. credential-helper
  shim.** `[url "http://…"] insteadOf = "https://github.com/"`
  captures push/fetch/clone/pull/ls-remote in one config knob;
  a credential helper would need separate wiring. Default:
  `insteadOf`.
- **Token-refresh story for the Anthropic OAuth token.** The
  token is ~1-year and there's no client-side refresh, so the
  proxy holds a static value. The 1-year blast radius is the
  cost, documented in
  [`claude-code-token-revocation.md`](../research/claude-code-token-revocation.md).
  No design change here; flagged for awareness.
- **`anthropics/claude-code#36998`.** Older claude-code
  versions bypassed `ANTHROPIC_BASE_URL` for some startup
  calls (auth validation, org lookup). Marked closed upstream;
  the implementation PR verifies with `strace -e connect`
  against the pinned claude-code build before trusting the
  isolation.

## References

- [`docs/research/agent-credential-proxy-landscape.md`](../research/agent-credential-proxy-landscape.md)
  — landscape research; this PRD is the build path that note
  recommends.
- [`docs/research/secret-minimization-over-dlp.md`](../research/secret-minimization-over-dlp.md)
  — architectural framing: why moving the credential matters
  more than scanning egress.
- PRD 0006: pipelock TLS interception — the
  `api.anthropic.com` TLS-MITM responsibility cred-proxy takes
  over.
- PRD 0008: Git gate — the credential-broker pattern this PRD
  reuses (gate holds creds, agent gets a rewritten URL, gate
  makes the upstream connection).
- [`anthropics/claude-code#36998`](https://github.com/anthropics/claude-code/issues/36998)
  — historic `ANTHROPIC_BASE_URL` bypass.
- [`go-gitea/gitea#16734`](https://github.com/go-gitea/gitea/issues/16734)
  — why Gitea uses `Authorization: token`, not `Bearer`.
- [`golang/go#28866`](https://github.com/golang/go/issues/28866)
  — the `HTTPS_PROXY` loopback bug; not hit here because we're
  a reverse proxy, not a forward proxy.
