# Headroom barely moves Claude Code subscription usage — and needs one or two proxies in VS Code

> Headroom compresses Claude Code requests in a local proxy. On 2.1.292 it moved subscription usage by under 5% either way, and cut API pricing by 4 to 16%.

- Source: https://oharu121.com/blog/headroom-claude-code-vscode-proxy-anthropic-base-url-tradeoff/
- Published: 2026-10-19T15:45:02+09:00
- Tags: Claude Code, Developer Tooling, Token Cost, VS Code

---
## Introduction

I heard that Headroom could cut Claude Code's token usage, and I wanted to know how it works before deciding whether to adopt it. My Claude Code runs as the VS Code extension on a subscription, with claude.ai connectors attached, so anything placed in front of the API had to leave those alone.

Headroom compresses requests in a local proxy, and there are two ways to put that proxy between Claude Code and the API. Pointing `ANTHROPIC_BASE_URL` at it switches off ToolSearch and Remote Control and inflates the context meter. A forward proxy that intercepts the connection to `api.anthropic.com` instead kept every feature I could measure, but takes two proxies.

**On my subscription, usage barely moved.** It landed between 5% less and 5% more. Most of what Headroom compresses is cached, and cached tokens count for little against a subscription's limits. At API prices the same runs cost 4 to 16% less, but the costs of either route far outweigh that, so I am not adopting it.

This article covers both routes and when the trade is worth making.

## How Headroom sits in the request path

Headroom is an open-source compression layer for coding agents, published as the Python package `headroom-ai`. It shrinks what goes into a model request (tool schemas, tool output, logs and conversation history) and keeps the originals on disk, so the model can fetch one back through a `headroom_retrieve` tool.

**It has to sit in the network path, because nothing inside Claude Code can rewrite a request.** A hook is a command Claude Code runs automatically at set points. Hooks such as `PreToolUse`, `PostToolUse` and `Stop` run around tool calls and turns, and none of them receives the API request body.

That leaves two ways in. Route 1 replaces the endpoint Claude Code sends to, which is what Headroom's own setup commands do:

```bash
headroom proxy --port 8787
ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude
```

Route 2 leaves the endpoint alone and intercepts the connection on its way out. It needs a second tool, covered in its own section below.

On route 1 the proxy forwards `/v1/messages`, the API path every model call goes to, to `api.anthropic.com`. **Subscription sign-in passes through unchanged.** A proxied session reported `apiKeySource: none`, the same as a direct one. That means it used no API key and stayed signed in through the subscription account. Every request succeeded.

*Figure — RequestPath: Route 1. Claude Code sends each request to the proxy named in ANTHROPIC_BASE_URL, which compresses it and forwards it to api.anthropic.com. Seeing a non-Anthropic URL, Claude Code also switches off Remote Control, ToolSearch and the Artifact tool.*

## Route 1: replacing `ANTHROPIC_BASE_URL`

Claude Code checks whether `ANTHROPIC_BASE_URL` points at `api.anthropic.com` and turns some features off when it does not. I asked Claude Code to start the same headless session, run with `claude -p` and no interactive interface, twice on version 2.1.292, once direct and once through the proxy, and compare the tools and commands each one received.

| | Direct | Through the proxy |
| --- | --- | --- |
| claude.ai connectors | 13 | 13 (12 of 13 runs) |
| `/schedule` | available | available |
| `ToolSearch` tool | available | missing |
| `Artifact` tool | available | missing |
| Remote Control | available | refused |

**Remote Control refuses to start** behind the proxy, and says why:

```bash
ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude remote-control
# Error: Remote Control is only available when using Claude via api.anthropic.com. ANTHROPIC_BASE_URL is set and does not point at api.anthropic.com, so this session is using a custom endpoint — unset it (or run in a shell without it) to use Remote Control.
```

`ToolSearch` is the tool that loads MCP tool definitions on demand instead of sending every one of them with every request. **Setting `ENABLE_TOOL_SEARCH=true` brings it back behind the proxy; the `Artifact` tool stays missing.**

**`/schedule` was in every proxied session**, and all 13 connectors loaded in 12 of 13 proxied runs. The thirteenth came up with no claude.ai connectors at all, which never happened in the 8 direct runs.

### `/context` misreports usage when the base URL points at the proxy

`/context` prints how much of the context window a session is using. **Behind the proxy, that number more than tripled:**

| Arm | `/context` total | MCP tool definitions |
| --- | --- | --- |
| Direct | 47.4k | 107.7k deferred, not counted |
| Proxy | 156.1k | 109.1k loaded |
| Proxy with `ENABLE_TOOL_SEARCH=true` | 191.5k | 137.9k loaded |

Deferring those definitions is `ToolSearch`'s job. With the base-URL check switching it off, Claude Code counts every MCP tool definition as loaded up front. **It also counts them before Headroom compresses anything**, so the model receives fewer tokens than the meter reports. Setting `ENABLE_TOOL_SEARCH=true` did not bring the deferred row back.

Billing follows what the model receives, so the inflated number costs nothing, and auto-compaction ignores it too. With the auto-compact window set to 120k, so the displayed usage crossed the threshold, two messages in a row triggered no compaction.

## Routing the VS Code extension with `wrap vscode-claude`

`headroom wrap claude` starts the proxy and launches the Claude Code CLI with `ANTHROPIC_BASE_URL` set on that one process, so it covers a terminal session and nothing else. The VS Code extension starts its own Claude Code binary from VS Code's environment, and Headroom has a separate subcommand for it. `headroom wrap vscode-claude --help` prints:

```text
Usage: headroom wrap vscode-claude [OPTIONS]

  Route VS Code's official Claude Code extension through Headroom.

  Run this from your project, reload VS Code after first setup, and keep this
  command running while using Claude Code. Authentication and model selection
  remain unchanged. Run `headroom unwrap vscode-claude` to restore settings.
```

The wrapper writes two entries into Claude Code's user `settings.json`: `ANTHROPIC_BASE_URL` set to `http://127.0.0.1:8787/p/<project>`, and `ENABLE_TOOL_SEARCH` set to `"true"`. **User settings apply to every Claude Code session on the machine**, so while that file carries the entry, every VS Code window and every terminal session goes through the proxy, whichever project the command ran in.

Headroom's documentation says a stopped proxy does not fall back: "A stopped proxy intentionally does not fall back to a direct Anthropic connection." **So if the wrapper process stops, every one of those sessions loses its connection.**

## Route 2: a forward proxy that leaves the base URL alone

A forward proxy is one a client hands all its outbound connections to, configured through `https_proxy`. [claude-rc-proxy](https://github.com/dthinkr/claude-code-workarounds/tree/main/tools/rc-proxy), shared in [a comment on a Headroom issue](https://github.com/headroomlabs-ai/headroom/issues/1734#issuecomment-5380785369), is one built for this. It decrypts only connections to `api.anthropic.com`, and passes every other host through as a raw, unread tunnel.

Inside that connection it sends only `/v1/messages` requests to a local backend. Everything else, including Remote Control's bridge and the OAuth calls, goes to the real Anthropic unchanged. **`ANTHROPIC_BASE_URL` stays unset, so the first-party check passes.**

*Figure — ForwardProxyPath: Route 2. Claude Code still sends to api.anthropic.com, so its check passes. rc-proxy decrypts that one connection, hands /v1/messages to Headroom, and sends everything else to Anthropic. Other hosts pass through unread.*

Claude Code read the source before anything ran, and found that **as published, it does not work with Headroom**. It is built for a model gateway, so it replaces the request's `Authorization` header with a gateway token and sends `/v1/messages` requests to the gateway at `127.0.0.1:8317`.

> **HEADS UP**
>
> **What a model gateway is**
>
> A model gateway, such as LiteLLM or CLIProxyAPI, is a server that takes model requests and sends them on with its own API keys, so the calls are billed to the gateway's accounts. Claude Code on a subscription uses none. It sends requests straight to Anthropic under your own account.

Pairing it with Headroom took a local fork that points it at Headroom's port and leaves the caller's own token in place:

```go title="tools/rc-proxy/main.go" del={1,5} ins={2}
upstreamPool = "127.0.0.1:8317" // CLIProxyAPI
upstreamPool = "127.0.0.1:8787" // Headroom
// …
r.URL.Scheme, r.URL.Host = poolURL.Scheme, poolURL.Host
r.Header.Set("Authorization", "Bearer "+poolToken)
```

The fork also drops the guard that refuses `/v1/messages` requests when no gateway token is set.

For rc-proxy to decrypt the connection, Claude Code has to trust the certificates it signs, so I made a certificate authority for the test (the root certificate that signs TLS certificates) and had only the Claude Code process trust it, through `NODE_EXTRA_CA_CERTS`. `ANTHROPIC_BASE_URL` stayed unset:

```bash
export https_proxy=http://127.0.0.1:9801 http_proxy=http://127.0.0.1:9801
export no_proxy=localhost,127.0.0.1,::1,datadoghq.com,statsig.com,sentry.io
export NODE_EXTRA_CA_CERTS=/tmp/rcproxy/ca/ca-cert.pem
```

*Figure — CertTrust: rc-proxy poses as api.anthropic.com with a certificate signed by the local CA, and Claude Code accepts it because it trusts that CA. For every other host, rc-proxy relays encrypted bytes it cannot read.*

I compared a direct session with one through both proxies, from the same shell:

| | Direct | Route 2 |
| --- | --- | --- |
| claude.ai connectors | 13 in 8 of 8 runs | 13 in 9 of 9 runs |
| `/schedule` | available | available |
| `ToolSearch` tool | available | available |
| `/context` total | 32.3k | 33.7k |
| `apiKeySource` | `none` | `none` |

**Nothing I could measure was lost.** The MCP tool definitions stayed deferred, so the meter reads what a direct session reads. Headroom's log showed 12 `/v1/messages` requests reaching it with 0 failures, so inference really did go through it.

## What the requests saved

I tested route 1 and route 2 in separate batches. Each batch ran several connection setups, called arms below, interleaved so they shared the same few minutes. Route 1 had three arms: direct, the base URL pointed at the proxy, and that plus `ENABLE_TOOL_SEARCH=true`. Route 2 had two: direct, and through both proxies.

Claude Code ran 21 requests for route 1 from its own shell, and I ran 16 for route 2 from my terminal. One prompt asks for a one-line reply. The other runs `git log --stat -40` and reads the output back. **Every answer matched across arms.**

*Figure — BenchmarkRuns: How the runs were interleaved. Within each round every arm runs once before the next round starts, so all arms share the same few minutes.*

Each run used one command, and its last line of output carries the usage. The two routes differ only in their environment variables:

```bash title="Route 1"
ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude -p "Reply with exactly: OK" \
  --output-format stream-json --verbose
```

```bash title="Route 2"
https_proxy=http://127.0.0.1:9801 NODE_EXTRA_CA_CERTS=/tmp/rcproxy/ca/ca-cert.pem \
  claude -p "Reply with exactly: OK" --output-format stream-json --verbose
```

### What counts toward a subscription's usage limit

A subscription has no per-token bill, so what matters is how fast a session uses up its limits. Anthropic's [usage-limit guidance](https://support.claude.com/en/articles/9797557-usage-limit-best-practices) says cached portions of a prompt "count less against your limits than new content", and publishes no weighting. [A third-party tracker of subscription accounts](https://wokey.ai/claude-usage-limits) estimates from a regression that a cache read uses "close to none" of the limit.

### Test 1: one-line prompt and `git log --stat`

Taking that at face value, the number to watch on a subscription is the uncached input plus the cache writes, with cache reads left out:

| Prompt | Route | Direct | Through Headroom | Change |
| --- | --- | --- | --- | --- |
| One-line | Route 1, `ToolSearch` missing | 16,245 | 11,220 | −31% |
| One-line | Route 1, `ToolSearch` restored | 16,245 | 16,993 | +5% |
| `git log --stat` | Route 1, `ToolSearch` restored | 18,434 | 17,779 | −4% |
| One-line | Route 2 | 13,636 | 12,989 | −5% |
| `git log --stat` | Route 2 | 17,271 | 17,118 | −1% |

**With `ToolSearch` kept, subscription usage moved between 5% less and 5% more**, which is inside the spread between runs of the same arm. The route 1 direct arm alone ranged from 14,090 to 17,452 on the one-line prompt.

The one large cut, 31%, is the route 1 arm that lost `ToolSearch`. **It comes from loading fewer tool definitions, not from compression.**

### Test 2: an eight-turn session with heavy tool output

One-line prompts are mostly the fixed prefix, so I also ran a workload closer to real use. Each session has eight turns, and each turn reads a large file or runs a command with long output. Every arm ran twice, interleaved, from my terminal.

| Arm | Round 1 | Round 2 | Mean vs direct |
| --- | --- | --- | --- |
| Direct | 40.9k | 44.8k | — |
| Route 1, `ToolSearch` restored | 34.0k | 37.2k | −17% |
| Route 2 | 46.1k | 39.1k | about 0% |

The numbers are subscription usage, uncached input plus cache writes, summed over turns 2 to 8. The first turn writes the session's cached prefix, and its size swung from 16k to 57k depending on whether the cache was still warm, so it is left out.

**Headroom left the tool output almost untouched.** Across the 76 requests it reported, it shrank message content by 1.9%. Most of its 12% headline saving was tool schemas, 344,690 tokens that sit in the cached prefix, **so it saved little that counts against the limit**.

Route 1's 17% is two runs against two, and route 2 sent the same requests through the same Headroom and saved almost nothing, so **I read it as run-to-run noise rather than compression**.

### What API pricing would see

**On API pricing, cache reads still cost a tenth of the input rate, so compressing the cached prefix saves something.** Below are Test 1's results repriced at API rates.

Total input counts every token the model received. Weighted cost prices them the way a cached prefix is billed, with cache writes at 1.25 times the base input rate and cache reads at 0.1 times.

Route 1:

| Prompt | Arm | Runs | Total input | Weighted cost |
| --- | --- | --- | --- | --- |
| One-line | Direct | 5 | 59,218 | 24,603 |
| One-line | Proxy, `ToolSearch` missing | 5 | 35,963 | 16,499 |
| One-line | Proxy, `ToolSearch` restored | 5 | 40,825 | 23,727 |
| `git log --stat` | Direct | 3 | 121,077 | 39,338 |
| `git log --stat` | Proxy, `ToolSearch` restored | 3 | 113,323 | 33,154 |

Route 2:

| Prompt | Arm | Runs | Total input | Weighted cost |
| --- | --- | --- | --- | --- |
| One-line | Direct | 5 | 44,550 | 20,153 |
| One-line | Route 2 | 5 | 37,985 | 18,735 |
| `git log --stat` | Direct | 3 | 94,313 | 29,100 |
| `git log --stat` | Route 2 | 3 | 81,255 | 27,822 |

Each figure is the median across that arm's runs. **The two direct baselines differ because the batches ran in different shells with different tools loaded, so compare each route only with the direct arm beside it.** The route 1 arm without `ToolSearch` looks cheapest at 33% off, and it pays for that with the missing tool.

*Figure — SavingsBars: Test 1, each route as a share of its own direct baseline. With ToolSearch kept, subscription usage stays within 5% of direct, while the API-priced cost drops 4% to 16%.*

**Most of the token cut lands on the cached prefix, which is already billed at a tenth of the price.** On the one-line prompt, route 1 with `ToolSearch` restored cut tokens by 31% and cost by 4%. **Route 2 cut tokens by 15% and cost by 7%.**

Headroom's own report said 36.5% for the route 1 batch and 15.0% for the route 2 batch. **But those figures count tokens alone, with no allowance for cached tokens being cheaper.**

## Why I do not recommend Headroom

**On a subscription, Headroom moved usage by less than 5% either way, and both routes cost something, so I do not recommend adopting it.**

Route 1 costs features:

1. Remote Control refuses to start.
2. `ToolSearch` disappears unless `ENABLE_TOOL_SEARCH=true` is set, and the `Artifact` tool disappears regardless.
3. The context meter reads three to four times the size of the request the model receives.
4. The claude.ai connectors failed to attach in one of 13 proxied runs.

Route 2 keeps them, and costs trust instead:

1. A local process decrypts every connection to `api.anthropic.com`, OAuth token included.
2. Pairing it with Headroom takes a fork of a third-party tool, which I would then maintain.
3. It stops working the day Anthropic pins its certificates, meaning Claude Code accepts only Anthropic's own.

**On either route, every Claude Code session on the machine depends on local proxies staying up**, and route 2 puts two of them in the chain.

Headroom's README claims 60–95% for JSON, but that case rarely arises in Claude Code. I had Claude Code run 36 commands that print JSON, and in 31 of them it filtered the output through `jq` itself, so the raw JSON never entered the context. Across my past 195 sessions, JSON was also only about 2% of tool output.

## Summary

- No Claude Code hook can rewrite the API request body, so Headroom compresses requests in a local proxy.
- Route 1 replaces `ANTHROPIC_BASE_URL`. On 2.1.292 it switches off Remote Control, `ToolSearch` and the `Artifact` tool, and inflates `/context`. `ENABLE_TOOL_SEARCH=true` restores `ToolSearch`, but the `Artifact` tool stays unavailable.
- Route 2 is a forward proxy such as claude-rc-proxy. It keeps connectors, `/schedule` and `ToolSearch`, and `/context` reports the right number, but it means forking and maintaining a third-party tool.
- On a subscription, with `ToolSearch` kept, **usage moved by under 5% either way**, in one-line prompts and in a session full of tool output, and most of the token cut lands on the cached prefix, which counts for almost nothing against the limits. At API prices it comes to roughly 4 to 16% less.
- Claude Code usually filters JSON through `jq` first, so the README's 60–95% rarely comes into play. **Neither route delivers a real cut in usage, so I do not recommend adopting Headroom.**

Testing the output of `claude -p --output-format stream-json` is enough to compare before deciding. The `init` line lists which tools the session has, and the `usage` line lists the tokens spent. Try it yourself.

## References

- [headroomlabs-ai/headroom](https://github.com/headroomlabs-ai/headroom)
- [Headroom CLI reference](https://headroomlabs-ai.github.io/headroom/cli/)
- [Use Headroom with Claude Code in VS Code](https://docs.headroomlabs.ai/docs/vscode-claude-code)
- [Remote Control disabled behind the proxy](https://github.com/headroomlabs-ai/headroom/issues/1779)
- [Comment: use a forward proxy that leaves ANTHROPIC_BASE_URL unset](https://github.com/headroomlabs-ai/headroom/issues/1734#issuecomment-5380785369)
- [claude-rc-proxy, in dthinkr/claude-code-workarounds](https://github.com/dthinkr/claude-code-workarounds/tree/main/tools/rc-proxy)
- [Connect Claude Code to tools via MCP, including ENABLE_TOOL_SEARCH](https://code.claude.com/docs/en/mcp)
- [Prompt caching pricing for cache writes and reads](https://docs.claude.com/en/docs/build-with-claude/prompt-caching)
- [Usage limit best practices, on how cached content counts against subscription limits](https://support.claude.com/en/articles/9797557-usage-limit-best-practices)
- [Claude usage limits measured on subscription accounts, including cache reads](https://wokey.ai/claude-usage-limits)
