# The local proxy

An optional listener inside tj serve that records what a cost policy would do. It ships in suggest mode, forwards every request unmodified, and never evaluates a policy against traffic it resolves as subscription.

---

`tj proxy` is an optional listener that runs inside `tj serve` and sits between your agent and its provider. It speaks the Anthropic `/v1/messages` and OpenAI `/v1/chat/completions` shapes, forwards each request to the real provider, and streams the response back.

It ships in **suggest mode only**. It records what a policy would have done. It blocks nothing, rewrites nothing, and changes no request. Off by default; you turn it on.

```bash
tj proxy enable
```

## What suggest mode means

Every request that reaches the listener is forwarded unmodified. For traffic that is eligible for a policy, the proxy also evaluates the policies you defined and records the outcome: `would_block`, a soft `near_ceiling` flag, or nothing. That record is what you read later. No request is delayed, altered or refused on the basis of it.

Enforcement is a separate piece and is not in the open tree. The evaluators never say "blocked"; they say "would block". Policies defined in OSS run **unvalidated**, since there is no certification engine here, so a suggestion is never implied to have been checked as safe.

## The pricing-mode gate

This is the first thing the proxy does with a request. It is an invariant, not a setting you can flip.

The proxy resolves the pricing mode for the targeted provider, reusing the same declared-plan logic the rest of tj uses. Then:

| Pricing mode | Path | What happens |
|---|---|---|
| `api` (usage-billed) | policy | the only traffic the policy engine ever evaluates |
| `subscription` | observe-only | forwarded unmodified, recorded, never evaluated |
| `local` | observe-only | not usage-billed, so there is no spend to evaluate |
| `unknown` | observe-only | the deliberate fail-safe |

Be precise about what "observe-only" means, because the wiring `tj proxy enable` writes is not plan-selective. It sets `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` in `~/.claude/settings.json`, which routes Claude Code's traffic through the listener whatever plan you are on. Subscription traffic therefore does arrive at the listener. What the gate decides is what happens next, and for subscription traffic the answer is: relay it and write down that you saw it.

Concretely, a subscription request is forwarded to the real provider with its own headers, including whatever credential it carried, minus the hop-by-hop headers any relay has to drop. The proxy records an observation of the gate decision: the provider, the plan tier, the pricing mode, the path chosen and the reason. It never builds a policy envelope for that request, so no policy evaluates it, no decision is recorded against it, and nothing about it is changed or delayed. Intercepting subscription-plan traffic to act on it is outside provider terms, and the gate is what keeps the policy path away from it.

One thing to know about that guarantee: the gate resolves the pricing mode from the plan tier you **declared** under `[budget.<provider>] plan`, not from anything it reads off the request. `tj init` prompts for it once, and you can edit it by hand. Declare `api` for a provider you actually use on a subscription and the gate resolves `api`, which is the policy row. If you hold both an API key and a subscription with one provider, check what you declared before enabling the proxy, and use `tj init --reconfigure` to change it.

What the proxy does not do with a credential: capture it, store it, log it, substitute one of its own, or reuse it for anything of its own. It relays and forgets.

The gate decision is a plain value object and is unit-tested on its own.

## Safety doctrine

**Pass-through is sacred.** An error anywhere in classification, policy evaluation or recording is logged and the request is forwarded anyway. Nothing the proxy does for its own purposes can break your traffic.

**The proxy holds no keys.** Your client's credentials are relayed to the provider on the request that carried them. The proxy injects none of its own, stores none, and logs none.

**`disable` only removes its own wiring.** `tj proxy enable` sets `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` in the `env` block of `~/.claude/settings.json`, the same file `tj init --claude-code` already manages, and `disable` removes them. It only ever removes a key whose value points at the tj proxy, so a base URL you set yourself is left alone.

**That is the only base-URL wiring written.** Claude Code reads that `env` block; nothing else does. Any other client, a Python or TypeScript agent on the Anthropic or OpenAI SDK for instance, keeps talking to the provider directly until you point its own base URL at `http://127.0.0.1:7392` yourself, through its client options or its own environment. `tj proxy status` prints the URL to use.

**The listener cannot outlive the server.** It starts and stops with `tj serve`'s own lifespan, in the same event loop. There is no separate daemon to orphan.

**`tj doctor` flags the footgun.** If the base-URL wiring still points at the proxy while the proxy is off, your traffic would hit a dead port. Doctor reports that orphaned wiring, and so does `tj proxy status`.

## Commands

```bash
tj proxy enable       # turn it on and wire the provider base URLs at it
tj proxy status       # config, killswitch state, and detected wiring
tj proxy disable      # turn it off and remove the wiring
tj proxy killswitch   # forward everything, classify nothing
tj proxy killswitch --off
```

For the machine-readable form of status, the `--json` flag is on the top-level command:

```bash
tj --json proxy status
```

The killswitch flips the proxy to pass-through-everything while the listener stays alive. Every request is then forwarded with no classification and no policy evaluation. `--off` releases it and normal classification resumes.

Restart `tj serve` after `enable`, `disable` or `killswitch` for the running listener to pick the change up.

## Turning it on during setup

```bash
tj init --enforce
```

Runs the same `tj proxy enable` wiring at the end of setup, then prints what it does and does not touch, including the pricing-mode gate. It composes with `--hooks`. If no tj config exists yet, it says so and asks you to run `tj init` first.

## Policies

A policy is data rather than code. It binds a registered evaluator to an optional provider or agent target:

```toml
[[policies]]
name = "anthropic-monthly-ceiling"
kind = "budget_cap"
enabled = true              # default
mode = "suggest"            # suggest only; enforce is scaffolded and gated off
target_provider = "anthropic"   # omit for any provider
target_agent = "my-agent"       # omit for any agent
params = { warn_at = 0.8 }      # budget_cap: the soft near_ceiling fraction
```

`budget_cap` is the concrete one. It reads the `[budget.<provider>] usd` ceiling you already configured and compares it against that provider's current-cycle spend. Over the ceiling it records `would_block`. At or above 80% of it, overridable with a `warn_at` param, it records a soft `near_ceiling` flag. Under, nothing. With no ceiling configured, or with current-cycle spend unavailable, it records that there was nothing to evaluate. It never guesses at a ceiling.

Decisions are persisted, and `tj policy decisions` shows recent ones. `tj policy list` shows the configuration.

The savings meter that rides alongside records **estimated recoverable** amounts: what a policy would have recovered had it been enforced. Nothing there is a realized saving, `realized` is always false, and dollar figures are api-only.

## Configuration

```toml
[proxy]
enabled = false
host = "127.0.0.1"
port = 7392
mode = "suggest"
killswitch = false
anthropic_base_url = "https://api.anthropic.com"
openai_base_url = "https://api.openai.com"
```

`mode` accepts `suggest`. The enforce path is scaffolded and gated off.

## A side benefit: stream usage

When a request asks to stream, the proxy arms a read-only tap on the response and records whether the trailing usage payload ever arrived. The bytes relayed to your client are unchanged. Those observations feed the `stream-usage` analyzer, which flags streamed calls whose token counts were never reported and are therefore missing from your spend total.

Whether you can read that finding depends on the window, not on the tap. `tj proxy enable` routes Claude Code, and `stream-usage` is one of the analyzers [skipped](/docs/optimize-more#when-an-analyzer-does-not-run) for a window an interactive coding agent dominates, so the observations get recorded and the analyzer never runs. Wiring an SDK client in does not by itself move that: what counts is the share of the window's traffic each side carries. The analyzer runs once the SDK side is the larger one, and it reports something only if a stream there actually closed without its usage payload.

## See also

- [CLI reference](/docs/cli) — `tj serve`, `tj init`, `tj doctor`
- [Configuration](/docs/configuration) — the rest of the TOML surface. `[proxy]` and `[[policies]]` are documented on this page, not there
- [Troubleshooting](/docs/troubleshooting) — when traffic stops flowing