Skip to the content.

Agent CLI gateway (Claude Code, Codex in beta)

Gateway mode lets an agent CLI point its base URL at PrivAiTe. On the way out, the request is scrubbed by the same engine and presets as the OpenAI-compatible endpoints, tool-call arguments included, and whatever auth the CLI itself sends is relayed verbatim upstream. On the way back, the real values are restored, streaming included. It is opt-in and off by default: with gateway.enabled: false the routes do not exist and nothing about the proxy changes.

Status. The Claude Code path (Anthropic Messages API: /v1/messages and /v1/messages/count_tokens) is the validated path: it was exercised live end to end against the real Anthropic API, and running with restore disabled proved the provider only ever received placeholders, restore and streaming included. The Codex path (OpenAI Responses API: /v1/responses) is beta: it passes the same test suite, but the Responses protocol has more moving parts and this path has had less live validation than Claude Code. Expect rough edges and please report what you hit.

How a request flows

sequenceDiagram
    participant CLI as Agent CLI (Claude Code, Codex)
    participant PVT as PrivAiTe gateway
    participant API as Provider API

    CLI->>PVT: request, with the CLI's own auth token
    Note over PVT: scrub at the single engine choke point<br/>message text, tool-call arguments, tool results
    PVT->>API: placeholders only, auth token relayed verbatim
    Note over API: the provider never sees the detected values
    API-->>PVT: response (streaming or not)
    Note over PVT: restore the real values, streaming included
    PVT-->>CLI: response with the real values back in place

Gateway routes carry the client’s own provider credentials: PrivAiTe neither injects nor validates any key there (PRIVAITE_API_KEYS applies to the OpenAI-compatible endpoints only). The mapping between real values and placeholders lives in memory for the length of the request, on your machine.

The gateway routes are unauthenticated, by design. Know what that means. With gateway.enabled: true, POST /v1/messages, POST /v1/messages/count_tokens and POST /v1/responses accept a request that carries no PrivAiTe key at all: the auth middleware skips exactly those paths, because the only credential in that request is the CLI’s own upstream token and there is no PrivAiTe key in it to verify. On top of that the server binds 0.0.0.0 by default (server.host) and applies no rate limit of any kind (the only inbound guard is server.max_request_bytes). So an exposed port plus gateway mode is an endpoint that anyone who can reach it can drive, spending your provider quota and being billed to whatever account the relayed token belongs to. Set server.host: "127.0.0.1", or keep the port off untrusted networks, before enabling gateway mode anywhere but localhost.

Enable it

gateway:
  enabled: true
  anthropic:
    base_url: "https://api.anthropic.com/v1"
  openai_responses:                                         # beta (Codex)
    base_url: "https://api.openai.com/v1"                   # API-key mode
    # base_url: "https://chatgpt.com/backend-api/codex"     # Codex subscription login

Enable the detection cache for agent sessions. Agent CLIs resend the whole conversation every turn, so without the cache every turn re-scans the entire history and the scrub cost grows with the context: on a large measured session the per-request scrub peaked at 42 s with Claude Code and 72 s with Codex, against a median of 1 to 3 s with the cache on. The tradeoff (PII-derived metadata, never values, staying in process memory up to the TTL) is spelled out in the README threat model; config details in the configuration reference.

pii:
  detection_cache:
    enabled: true

Claude Code

ANTHROPIC_BASE_URL=http://localhost:8400 claude

That is the whole setup. Claude Code sends its own login with each request; the gateway relays it as-is to gateway.anthropic.base_url.

Codex (beta)

Codex only speaks the Responses API, and the /v1/responses route is the beta part of the gateway (see the status note above). Add a custom provider to ~/.codex/config.toml:

model_provider = "privaite"

[model_providers.privaite]
name = "PrivAiTe"                # PrivAiTe Responses support is beta
base_url = "http://localhost:8400/v1"
wire_api = "responses"
requires_openai_auth = true      # Codex login relayed as-is
# env_key = "OPENAI_API_KEY"     # API-key mode instead: use this line, drop requires_openai_auth

With requires_openai_auth, also set the gateway upstream to the backend Codex actually talks to (https://chatgpt.com/backend-api/codex, commented in the YAML above). For API-key mode, keep the default https://api.openai.com/v1; that is the durable, documented upstream.

What is scanned (and what is not)

Anthropic Messages. Scanned: messages[] content when it is a plain string; text blocks; tool_use input (the tool-call-argument leak) and the server_tool_use / mcp_tool_use input the client echoes back after a restore; tool_result and mcp_tool_result content (a bare string, a nested block, or a list of both); document blocks (title, context, and a text or content source); search_result blocks (title, source, content). A block type this build does not know is also scanned rather than relayed raw: its allowlisted plaintext fields go through the engine, its input/output payload is walked leaf by leaf, and its content and a dict source follow the same rules as a known block. Not scanned: the system field (relayed verbatim, see below), tools/tool_choice definitions, and JSON object keys. Relayed byte-for-byte: thinking and redacted_thinking blocks (Anthropic rejects modified thinking blocks echoed back on a later turn, so they pass through untouched in both directions), the binary/pointer blocks, base64/url/file document sources, and on an unknown block everything outside the allowlist (a source.data blob, a signature, encrypted_content, ids).

OpenAI Responses (beta). Scanned: input as a plain string, or item by item: the content of an item that carries both a role and a content (including the text and refusal fields of its parts, and bare strings in the part list); function_call arguments (parsed as JSON, scrubbed value by value, re-encoded); custom_tool_call input; the typed data field of a typed item; the output of any *_output item (this is where a file the agent read comes back, walked leaf by leaf); the text-bearing fields (output, arguments, input, text, reason) and the content of an item shape this build does not know; bare strings in the input list; and prompt.variables (the prompt template’s own id and version are not user text and are left alone). Not scanned: the top-level instructions field (relayed verbatim, see below), tools definitions, and JSON object keys. Relayed byte-for-byte: the opaque item types (encrypted reasoning and compaction, generated images, server-side pointers, tool listings) and the binary content/output parts.

Exact lists (pinned to the code by a test)

These are the frozensets the scrubber actually uses; tests/test_gateway/test_gateway_docs.py fails if this page and the code drift apart, in either direction.

The unscanned system and instructions fields matter in practice: they are the agent’s own prompt, and Claude Code injects your CLAUDE.md and project context there, so PII inside those reaches the provider. Keep secrets and personal data out of them. They are read for policy even so: both go through the same block_entities gate, so a blocked type sitting in the agent’s prompt rejects the request instead of being relayed.

Restore covers both streaming and non-streaming responses. The same fail-closed policy applies: if scrubbing fails, the request is rejected and nothing is forwarded.

Measured, not promised

The measurements in this section are historical (PrivAiTe 0.4.1). Unreleased source update: structured-secret rules now target the log fields below, and overlap resolution respects redaction and block policies. See detection. The original live-agent results remain unchanged; offline regression replays are a separate measurement.

The agent-workflow benchmark drives real Claude Code and Codex sessions over a repository with 24 planted PII values and secrets and records every byte the provider actually receives. Directly, Claude Code sent 24/24 planted values to the provider and Codex 20/24. Through the gateway with the default onnx preset, 0/24 reached the provider on that fixture; on a larger, more realistic session, 2 of 24 still got through. Both are secrets in key=value log lines, and the mechanism is now measured rather than guessed. It is a detection miss, not a routing bug: the gateway traversed and scrubbed those exact lines (they arrive at the provider with <DATE_TIME_n> placeholders already substituted into them), the detector simply did not flag the two values.

What the miss actually depends on is surrounding context, not input size:

It lands where the benchmark already says the detector is weakest (SECRET recall 71.4% on the comparison corpus). Read the 2 of 24 as a strong measured reduction, never as zero leaks, and read it as a floor rather than a ceiling: one of the four database-URL password occurrences is held back only by a Presidio EMAIL_ADDRESS false positive scoring 1.0 over the URI userinfo, so that password was typed and placeholdered as an email (and therefore reversible) rather than redacted as a secret. The results page also carries the latency and cache measurements behind the recommendation above.

Known behaviors (from live validation)

Observed in real Claude Code and Codex sessions through the gateway. These are fidelity notes and beta edges, not leaks: in each case the real values stayed on the machine.

Honest limits