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%.

The Headroom logo, a pair of mint-green square brackets around two horizontal bars, beside the mint-green Headroom wordmark on a near-black card
On this page

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:

Terminal window
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.

Route 1: Headroom as the base URLClaude Code, running as the VS Code extension or the CLI, sends its request to the address in ANTHROPIC_BASE_URL, 127.0.0.1 port 8787. There the Headroom proxy compresses tool schemas, tool output and history, keeps the originals on disk, and forwards /v1/messages to api.anthropic.com. Below, Claude Code checks the base URL, finds it is not api.anthropic.com, and switches three things off: Remote Control refuses to start, the ToolSearch tool is removed unless ENABLE_TOOL_SEARCH=true is set, and the Artifact tool is removed. On version 2.1.292, /schedule is kept, and the claude.ai connectors loaded in 12 of 13 runs.Claude CodeVS Code extensionor CLIANTHROPIC_BASE_URL127.0.0.1:8787Headroom proxycompresses tool schemas,tool output and history;keeps originals on disk/v1/messagesapi.anthropic.comClaude Code finds the base URL is not api.anthropic.com✕Remote Control refuses to start✕ToolSearch removed (ENABLE_TOOL_SEARCH=true restores it)✕Artifact tool removed✓/schedule kept; claude.ai connectors loaded in 12 of 13 runs (2.1.292)
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.

DirectThrough the proxy
claude.ai connectors1313 (12 of 13 runs)
/scheduleavailableavailable
ToolSearch toolavailablemissing
Artifact toolavailablemissing
Remote Controlavailablerefused

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

Terminal window
IN
ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude remote-control
OUT
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 totalMCP tool definitions
Direct47.4k107.7k deferred, not counted
Proxy156.1k109.1k loaded
Proxy with ENABLE_TOOL_SEARCH=true191.5k137.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:

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, shared in a comment on a Headroom issue, 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.

Route 2: a forward proxy in front of HeadroomClaude Code runs with ANTHROPIC_BASE_URL unset, so its first-party check passes, and sends its traffic to rc-proxy through https_proxy on 127.0.0.1 port 9801. rc-proxy decrypts only connections to api.anthropic.com. It hands /v1/messages requests to Headroom on port 8787, which compresses them and forwards them to api.anthropic.com, and sends everything else, such as Remote Control and OAuth, straight to api.anthropic.com. Every other host, connectors and telemetry included, passes through as a raw tunnel and is never decrypted.Claude Codebase URL unset✓ check passesrc-proxy127.0.0.1:9801decrypts onlyapi.anthropic.comHeadroom127.0.0.1:8787compresses the requestapi.anthropic.cominference,Remote Control, OAuthhttps_proxy/v1/messageseverything elsecompressedEvery other host, connectors and telemetry included, passes through as a raw tunnel and is never decrypted.
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.

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

tools/rc-proxy/main.go
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:

Terminal window
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
Which connections rc-proxy can readTwo rows. In the first, Claude Code connects to api.anthropic.com through rc-proxy. rc-proxy presents a certificate for api.anthropic.com signed by the local certificate authority, which Claude Code trusts through NODE_EXTRA_CA_CERTS, so this first TLS connection is decrypted. rc-proxy hands /v1/messages to Headroom, which compresses it and opens a second TLS connection to api.anthropic.com with Anthropic’s real certificate. In the second row, Claude Code asks rc-proxy to CONNECT to gmailmcp.googleapis.com. rc-proxy only copies the encrypted bytes, so a single TLS connection with Google’s real certificate runs end to end, and rc-proxy cannot read it.api.anthropic.com: decryptedClaude Codetrusts thelocal CArc-proxyposes asapi.anthropic.comand decryptsHeadroomcompressesapi.anthropic.comTLS 1/v1/messagesTLS 2TLS 1 · a local-CA certificate for api.anthropic.com, so Claude Code believes it is talking to AnthropicTLS 2 · Anthropic’s real certificateEvery other host: passed throughClaude Coderc-proxycopies bytesonlygmailmcp.googleapis.comGoogle’s real certificateCONNECTencryptedone TLS connection, end to endrc-proxy reads only the hostname in CONNECT, so this traffic stays unreadable to it.
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:

DirectRoute 2
claude.ai connectors13 in 8 of 8 runs13 in 9 of 9 runs
/scheduleavailableavailable
ToolSearch toolavailableavailable
/context total32.3k33.7k
apiKeySourcenonenone

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.

How the benchmark runs were interleavedTwo timelines. Route 1, run by Claude Code in its own shell, has 21 runs: five rounds of the one-line prompt, each running the direct, base-URL-proxy and ENABLE_TOOL_SEARCH arms once, then three rounds of the git log prompt, each running the direct and ENABLE_TOOL_SEARCH arms once. Route 2, run in my terminal, has 16 runs: five rounds of the one-line prompt and three rounds of the git log prompt, each running the direct arm and the rc-proxy to Headroom arm once. Each run’s usage comes from the last line of claude -p --output-format stream-json, and each arm reports its median.Route 1 · run by Claude Code in its own shell · 21 runsone-line promptdirectbase URL → proxy+ ToolSearch× 5 roundsgit log --statdirect+ ToolSearch× 3 roundsRoute 2 · run by me in my terminal · 16 runsone-line promptdirectrc-proxy → Headroom× 5 roundsgit log --statdirectrc-proxy → Headroom× 3 roundsEach run’s usage comes from the last line of claude -p --output-format stream-json. Each arm reports its median.
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:

Route 1
ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude -p "Reply with exactly: OK" \
--output-format stream-json --verbose
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 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 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:

PromptRouteDirectThrough HeadroomChange
One-lineRoute 1, ToolSearch missing16,24511,220−31%
One-lineRoute 1, ToolSearch restored16,24516,993+5%
git log --statRoute 1, ToolSearch restored18,43417,779−4%
One-lineRoute 213,63612,989−5%
git log --statRoute 217,27117,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.

ArmRound 1Round 2Mean vs direct
Direct40.9k44.8k—
Route 1, ToolSearch restored34.0k37.2k−17%
Route 246.1k39.1kabout 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:

PromptArmRunsTotal inputWeighted cost
One-lineDirect559,21824,603
One-lineProxy, ToolSearch missing535,96316,499
One-lineProxy, ToolSearch restored540,82523,727
git log --statDirect3121,07739,338
git log --statProxy, ToolSearch restored3113,32333,154

Route 2:

PromptArmRunsTotal inputWeighted cost
One-lineDirect544,55020,153
One-lineRoute 2537,98518,735
git log --statDirect394,31329,100
git log --statRoute 2381,25527,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.

Proxied requests as a share of direct requests, by routeFive comparisons, each against the direct requests of its own batch, shown as total input tokens, weighted cost and subscription usage, which leaves out cache reads. Route 1, one-line prompt with ToolSearch missing: 61% of the tokens, 67% of the cost and 69% of the subscription usage. Route 1, one-line prompt with ToolSearch restored: 69% of the tokens, 96% of the cost and 105% of the subscription usage. Route 1, git log prompt with ToolSearch restored: 94% of the tokens, 84% of the cost and 96% of the subscription usage. Route 2, one-line prompt: 85% of the tokens, 93% of the cost and 95% of the subscription usage. Route 2, git log prompt: 86% of the tokens, 96% of the cost and 99% of the subscription usage.Total input tokensWeighted costSubscription usagedirect = 100%Route 1 · one-line promptToolSearch missing61%67%69%Route 1 · one-line promptToolSearch restored69%96%105%Route 1 · git log --statToolSearch restored94%84%96%Route 2 · one-line promptforward proxy85%93%95%Route 2 · git log --statforward proxy86%96%99%
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

Share this article