claude-code-cache-fix

作者 cnighswonger已验证

Fixes prompt cache regression in Claude Code that causes up to 20x cost increase on resumed sessions

418
Stars
35
Forks
JavaScript
语言
2026/8/23
添加时间

⚠️ 第三方软件声明

本 Skill 为第三方开源软件,独立托管于 GitHub。SkillTip 仅为信息目录,不控制或维护底层仓库。所显示的安全检查为自动化且范围有限,安装前请自行审查源码。

阅读服务条款

安装

添加到你的 Claude Code skills 目录:

# Add to your Claude Code skills
git clone https://github.com/cnighswonger/claude-code-cache-fix

快速入门

使用 claude-code-cache-fix 等 Skills 的指南。

安全报告

已验证

上次扫描:—

{
  "status": "PASSED",
  "issues": []
}

README.md

claude-code-cache-fix

npm Node.js License: MIT GitHub stars

English | 中文 | 한국어 | Français | Português

Cache optimization proxy for Claude Code. Fixes prompt cache bugs that cause excessive quota burn, stabilizes the request prefix, and monitors for silent regressions. Works with all CC versions including the v2.1.113+ Bun binary.

This README documents current main; release availability is noted per feature.

What it does to your traffic

A local proxy sits between Claude Code and Anthropic. Before you read further, here is exactly what that means — the full treatment is in Security model.

  • Binds to 127.0.0.1 by default.
  • Forwards Claude Code traffic to Anthropic. On the default path it makes no other outbound calls — telemetry is written to local files under ~/.claude/, never sent anywhere. Two opt-in features do perform their own egress, both off unless you enable them: OAuth refresh (CACHE_FIX_OAUTH_REFRESH=on) posts to Anthropic's token endpoint, and forward-proxy download acceleration re-issues release downloads to downloads.claude.ai / storage.googleapis.com.
  • Can read and rewrite POST /v1/messages. That capability is the cache repair — there is no version of this that works without it.
  • It is idempotent: if nothing needs fixing, the request passes through unmodified. It normalizes request structure (block order, fingerprint, TTL); it does not modify your conversation.
  • Each transform is one file in proxy/extensions/, readable in isolation.
  • Independently assessed as a legitimate tool by @TheAuditorTool (2026-04-14).

Forward-proxy mode (--remote-control) additionally terminates TLS for api.anthropic.com using a locally-generated CA, which your client must trust. Everything else is blind-tunnelled. That mode is opt-in and off by default.

Do you need this?

Install or test it if: resumed or long-running sessions show repeated cache_creation_input_tokens spikes; your cache-read ratio is low or unstable; you see unexpected TTL 5m downgrades, thinking-desync 400s, or image-retry storms; or one of the non-cache surfaces documented below applies.

You can skip it if: your sessions already hold a stable high cache-read ratio; you rarely resume long sessions; you are not under quota pressure; or you would rather not place a local proxy in the API path. All four are good reasons not to install this.

If you are not sure which applies, measure it — you do not need this project installed to find out.

Check whether you have this problem

Claude Code already records per-request cache accounting in its own session transcripts, so you can measure your cache health right now, before installing anything.

# Replace <session-uuid>, or use a glob to pick your most recent session.
jq -r 'select(.message.usage.cache_read_input_tokens != null) |
  "\(.requestId)\t\(.message.usage.cache_read_input_tokens) \(.message.usage.cache_creation_input_tokens)"' \
  ~/.claude/projects/*/<session-uuid>.jsonl |
  sort -u -k1,1 | cut -f2 |
  awk '{n++; r+=$1; c+=$2}
       END {if (n==0) print "no usage rows found — check the session path";
            else printf "requests=%d cache_read=%d creation=%d read-ratio=%.0f%%\n", n, r, c, 100*r/(r+c)}'

sort -u -k1,1 counts each API call once — Claude Code writes multiple transcript rows per request, and not always the same number of times per request (ArkNill's analysis). Summing raw rows weights each call by its own duplicate count. Two independent sweeps of the local transcripts on one machine (2026-08-02) agreed on the shape: short sessions are where this bites — over half of sessions under 20 requests shifted by a point or more without the dedup, worst case 41 points, while long sessions were almost all sub-point (3 of ~37). Short sessions are exactly what a first-time reader will run this against.

Reading the result:

  • Fewer than ~20 requests: the number is meaningless. A cold start has nothing to read yet, so creation dominates and every healthy session looks broken. Use a long or resumed session.
  • Sustained low ratio on a long session, or creation spiking on every --resume — that is the problem this project exists to fix.
  • High ratio on a long session — you do not need this. See Do you need this? above.

Current advisories

v4.0.0 — Local HTTP proxy with a pipeline of cost-impact and observability extensions. Two long-standing defaults flipped: thinking-block-sanitize v1 is on by default (mitigates the thinking-desync 400 wedge — #63147) and in-process extension hot-reload is opt-in (CACHE_FIX_HOT_RELOAD=on). A/B baseline (v3.0.0 on v2.1.117): 95.5% cache hit rate through proxy vs 82.3% direct on first warm turn. Full release notes →

Opus 4.7 advisory: Metered data shows 4.7 burns Q5h quota at ~2.4x the rate of 4.6 for equivalent visible token counts (independently confirmed by @ArkNill). Two factors: a new tokenizer (up to 35% more tokens, documented) and adaptive thinking overhead (~105%, not documented in usage response). The Q5h impact compounds into Q7d — the weekly quota ceiling that most heavy users will hit first. Workaround: CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1 reduces burn by ~3.3x but may reduce quality on complex tasks. See Discussion #25 (initial observation) and Discussion #42 (controlled A/B data + Q7d analysis).

Quick Start: Proxy (recommended)

The proxy works with any CC version — Node.js or Bun binary. It sits between Claude Code and the Anthropic API, applying cache fixes as composable extensions.

# Install
npm install -g claude-code-cache-fix

# Start the proxy (runs on localhost:9801)
node "$(npm root -g)/claude-code-cache-fix/proxy/server.mjs" &

# Launch Claude Code through it
ANTHROPIC_BASE_URL=http://127.0.0.1:9801 claude

That's it. The proxy applies its default extension pipeline automatically. No wrapper scripts, no NODE_OPTIONS, no preload.

Forward-proxy mode (keeps Remote Control working)

The quick-start above is reverse-proxy mode: you point ANTHROPIC_BASE_URL at the proxy. That is simple, but on Claude Code >= 2.1.196 a non-Anthropic ANTHROPIC_BASE_URL disables Remote Control (/remote-control), /schedule, and claude.ai MCP connectors (CC treats any custom base URL like a Bedrock/Vertex gateway). If you rely on those features, use forward-proxy mode instead.

In forward-proxy mode the proxy sits in front of the real api.anthropic.com as an HTTPS_PROXY. Claude Code's base URL stays api.anthropic.com, so Remote Control keeps working, while the proxy still sees and transforms /v1/messages.

# Start the proxy in forward-proxy mode
CACHE_FIX_FORWARD_PROXY=on node "$(npm root -g)/claude-code-cache-fix/proxy/server.mjs" &
# It prints the two env vars to wire the client, e.g.:
#   export HTTPS_PROXY=http://127.0.0.1:9801
#   export NODE_EXTRA_CA_CERTS=~/.claude/cache-fix-ca/ca.pem

# Launch Claude Code through it (leave ANTHROPIC_BASE_URL UNSET)
HTTPS_PROXY=http://127.0.0.1:9801 \
NODE_EXTRA_CA_CERTS=~/.claude/cache-fix-ca/ca.pem \
  claude

Or let the launcher do both steps for you with --remote-control:

# Spawns the proxy with CACHE_FIX_FORWARD_PROXY=on and wires the client
# (HTTPS_PROXY + the MITM CA, ANTHROPIC_BASE_URL left unset) automatically.
cache-fix-proxy --remote-control

The --remote-control flag is the one-command equivalent of the manual wiring above: it starts the proxy in forward-proxy mode, waits for the CA, and launches claude pointed at HTTPS_PROXY with NODE_EXTRA_CA_CERTS set (and adds 127.0.0.1,localhost,::1 to NO_PROXY so local services — e.g. HTTP/SSE-transport MCP servers on localhost — bypass the proxy rather than being routed at it; any existing NO_PROXY is preserved). Without the flag the launcher stays in reverse-proxy mode (sets ANTHROPIC_BASE_URL), unchanged. Two things worth knowing: Remote Control does a trusted-device enrollment on first connect that can need a few /remote-control retries (a Claude Code step that runs upstream, not a proxy failure); and enabling RC on an already-warm session costs a single prompt-cache rebuild (RC adds an anthropic-beta the cache keys on), so if you want RC, launching with --remote-control from the start avoids that one-time flip. cache-fix-proxy --help documents both.

If you wire forward-proxy mode manually (setting HTTPS_PROXY yourself instead of using --remote-control), set NO_PROXY=127.0.0.1,localhost,::1 as well, or local HTTP-transport MCP servers and other localhost services will be routed at the cache-fix proxy and fail. stdio-transport MCP servers are unaffected (they use pipes, not the network).

How it works: the proxy also handles HTTP CONNECT. It MITMs only the upstream host (api.anthropic.com), terminating TLS with a locally-generated CA so it can run the same extension pipeline, and blind-tunnels every other CONNECT (mcp-proxy, telemetry, npm, ...) untouched. On first start it generates a CA under $CLAUDE_CONFIG_DIR/cache-fix-ca/ (default ~/.claude/cache-fix-ca/; override with CACHE_FIX_CA_DIR); the client must trust it via NODE_EXTRA_CA_CERTS. A WebSocket/Upgrade to the upstream host (e.g. /voice) is relayed to upstream as-is. Because base URL stays api.anthropic.com, all of /api/oauth/*, /v1/agents, Remote Control credential fetches, etc. pass through untouched and RC stays enabled.

Corporate proxy chaining works the same as reverse mode: set HTTPS_PROXY/HTTP_PROXY for the proxy's own upstream egress (the proxy dials api.anthropic.com through it). The client's HTTPS_PROXY points at the cache-fix proxy; the cache-fix proxy's HTTPS_PROXY (in its own env) points at the corporate proxy.

Crash semantics on a shared proxy. In forward-proxy mode the proxy MITMs the whole upstream host, so an in-flight Claude Code session is wired to this port and cannot fail over. To keep one bad request from taking the process down, a successful forward-proxy attach installs process-wide uncaughtException/unhandledRejection handlers that log and keep serving instead of crashing. These are scoped to forward mode (a reverse-only proxy keeps Node's default crash-on-uncaught semantics, letting its supervisor restart it) and are removed when the last forward instance closes. The tradeoff: on a shared / multi-tenant proxy, enabling forward mode changes crash behavior for every client on that instance while the mode is on — a fatal bug is swallowed rather than surfaced to a supervisor. If you run one proxy for many sessions, weigh that against a supervised per-session model.

Running it persistently. The ... node .../proxy/server.mjs & above is fine for a quick try, but a backgrounded process is not supervised: it does not restart if it crashes or if the machine reboots. To run forward-proxy mode as a managed service (auto-restart, start-on-login), use the same install-service path described under Running as a service — just set the flag at install time so it is baked into the unit:

CACHE_FIX_FORWARD_PROXY=on cache-fix-proxy install-service

The generated systemd unit / launchd agent carries CACHE_FIX_FORWARD_PROXY=on, so the service starts the proxy in forward-proxy mode and keeps it up (systemd Restart=on-failure plus the healthcheck timer; launchd KeepAlive).

The service only manages the proxy end. It does not — and cannot — set anything on your claude client, which is a separate process. You still wire the client yourself in whatever shell launches claude, using the two values from the forward-proxy quick-start above:

  • HTTPS_PROXY — where the proxy listens: http://127.0.0.1:<port> (default port 9801, or your CACHE_FIX_PROXY_PORT).
  • NODE_EXTRA_CA_CERTS — the CA the proxy generated on first start: ~/.claude/cache-fix-ca/ca.pem (or $CACHE_FIX_CA_DIR/ca.pem).

Three ways to wire it, depending on how broadly you want the vars to apply.

If anything else on this host also MITMs api.anthropic.com — a corporate TLS-inspecting agent, an account-switching pin proxy — do not use these recipes. NODE_EXTRA_CA_CERTS takes one file, so pinning it to our CA alone silently untrusts every other component. Use --remote-control, which publishes into ca-trust.d/ and consumes the merged bundle instead. See Coexisting with another MITM.

# a) per-invocation — scoped to just this claude run
HTTPS_PROXY=http://127.0.0.1:9801 \
NODE_EXTRA_CA_CERTS=~/.claude/cache-fix-ca/ca.pem \
  claude

# b) whole shell — add to ~/.zshrc / ~/.bashrc (every HTTPS in that shell goes
#    through the proxy; harmless since non-anthropic hosts are blind-tunneled,
#    but that shell's HTTPS breaks if the proxy is ever down)
export HTTPS_PROXY=http://127.0.0.1:9801
export NODE_EXTRA_CA_CERTS=~/.claude/cache-fix-ca/ca.pem

# c) scoped to claude only — a shell function (recommended; avoids b's blast radius)
claude() {
  HTTPS_PROXY=http://127.0.0.1:9801 \
  NODE_EXTRA_CA_CERTS=~/.claude/cache-fix-ca/ca.pem \
    command claude "$@"
}

Coexisting with another MITM on the same machine (ca-trust.d)

NODE_EXTRA_CA_CERTS takes exactly one file. If anything else on the host also MITMs api.anthropic.com and also sets that variable — a corporate agent, an account-switching pin proxy — the last writer wins and every other CA is silently untrusted. Measured 2026-07-30: two such components on one machine took turns breaking each other's TLS, with no error attributable to either.

So --remote-control does not simply assign the variable. It:

  1. Publishes our CA to <config>/ca-trust.d/ccf.pem — our own filename only, never a sibling's, rewritten every launch (the proxy regenerates its CA whenever the CA dir is wiped, and a stale pem advertises a key nothing signs with), skipped when the bytes already match, and written via temp + rename so a reader never sees a half-written file.
  2. Reads <config>/ca-trust.pem — a merged bundle built by exactly one external writer from the ambient/corporate roots plus every published ca-trust.d/*.pem — and points NODE_EXTRA_CA_CERTS at it.

<config> is CLAUDE_CONFIG_DIR or ~/.claude. We never write the merged bundle: merging requires finding the ambient corporate roots, which is environment-specific (a Linux host may keep them outside the bundle a shell points at; a Mac keeps them in the keychain), and two components both rebuilding it would race one output.

The bundle is used only if node, handed that file, will actually verify our proxy's leaf. The launcher does not predict that — it asks: a child process with NODE_EXTRA_CA_CERTS set from birth stands up a TLS server holding our leaf and connects to it. Only a bundle from which the loader really loaded our CA can complete that handshake.

A bundle failing that is worse than no bundle — it would make the client distrust the very proxy it is being routed through, so every request fails TLS rather than merely losing some other component's CA.

Why ask rather than parse. The previous version modelled node's loader in a regex: base64 quanta, padding position, dash runs in markers, which of ten whitespace characters openssl tolerates. It took five review rounds and was still wrong in both directions on a real bundle — accepting one node loads nothing from, and refusing one node loads fine. The rule it was reaching for turns out not to be expressible from outside: an identical tear is recovered or fatal depending only on whether its truncated body happens to be complete DER, which is a question about bytes the parser cannot answer. The loader can, in one spawn (~25 ms over a bare node -e '' — measured, 40 interleaved pairs: 17.3 ms bare, 42.4 ms probed). What that is 25 ms of: a --remote-control launch is ~520 ms end to end, of which ~493 ms is forking the proxy and waiting for it to listen. So the probe is ~8% of a launch and very nearly all of the CA work.

Three outcomes, never two. ok, not ok, and unknown — the last meaning the probe could not be run at all. A guard that answers "unusable" when it could not ask drops every corporate root on a machine whose bundle was fine.

A damaged merge does not cost the other publishers their CAs. The damage lives in the merge, not in the files that fed it, so the launcher rebuilds from the ca-trust.d/ publishers that still work rather than falling back to its own CA alone. The saving is one certificate per surviving publisher: measured on this box (ours plus one peer), one certificate under the old fallback against two under the rebuild; on a three-publisher host, one against three.

Both paths are fixed names under <config>, deliberately with no env override of their own. They are two halves of one rendezvous: a knob on either half alone lets a participant publish where no builder looks, or read a file no builder writes, while still appearing to implement the contract. CLAUDE_CONFIG_DIR already relocates the pair, and it moves both halves together.

Note the limit of what a consumer checks: intact, and carries my CA. Whether the bundle is complete — that no corporate root went missing — is the builder's guarantee, and a consumer must not act on it even where it could.

That is a design choice, not a missing capability, and the distinction matters because the other reading is an invitation: someone adds the previous bundle as state, believes the limitation is lifted, and adds a floor. It would still be wrong. A shrink is legitimate whenever a root is retired or a component is uninstalled, and only the builder knows which happened — so a reader holding both bundles still cannot tell a regression from a fact. Measured: a legitimate bundle is 5 certs on one machine here and 168 on another, so any floor that catches narrowing on one host rejects a healthy bundle on the next.

This is a cooperative convention among same-user processes, not a trust boundary. The check proves parses, and carries us — never contains only approved writers. Anyone who can write <config> can hand us a well-formed bundle holding our CA plus their own and it will be accepted, exactly as they could already have replaced ca-trust.d/ccf.pem, the CA dir, or this file. The contract defends against components accidentally untrusting each other, which is the failure that actually happens; it does not defend against a local attacker, who has simpler routes.

CACHE_FIX_DOWNLOAD_REWRITE breaks claude update — leave it off

CACHE_FIX_DOWNLOAD_REWRITE=on reads like a pure performance knob. It is not: turning it on disables claude update entirely on that host. Rewriting a download URL means reading it, which means MITM-ing downloads.claude.ai — and the release-channel client pins public roots only and rejects any private CA, so the version check dies before a byte is downloaded:

Failed to fetch version from .../claude-code-releases/latest after 3 attempt(s):
  unable to verify the first certificate

Measured with openssl s_client -proxy 127.0.0.1:9901 -connect downloads.claude.ai:443 -servername downloads.claude.ai:

CACHE_FIX_DOWNLOAD_REWRITEleaf CNverify
onapi.anthropic.comcode 21
offdownloads.claude.ai (WR3 / GTS Root R1)code 0

Two things make this worse than it first looks:

  • It cannot be narrowed to the binary download. MITM is decided per host at CONNECT time, and the version check shares downloads.claude.ai with the download itself. It is all-or-nothing per host.
  • No client-side override reaches that client. HTTPS_PROXY / ALL_PROXY, /etc/hosts, /etc/resolv.conf, and NODE_EXTRA_CA_CERTS were each disproved against a control on the identical path — a local resolver logged 0 queries and a TCP forwarder logged 0 connects across a full claude update, while a plain node https.get through that same forwarder returned 200. So no amount of CA injection can make the rewrite work. Only not intercepting works.

Other hosts are unaffected: github.com through the same proxy returns its real certificate and verifies. The flag is off by default; keep it that way unless you are prepared to update Claude Code some other way.

What the proxy does

On every /v1/messages request, the pipeline runs an ordered chain of extensions covering cache stability, observability, thinking-desync mitigation, image, microcompact, breakpoint, bootstrap-channel, and other surfaces. Several are gated behind env vars documented in their own sections below; bootstrap-channel handling defaults to audit mode. The headliners:

ExtensionWhat it fixes
fingerprint-stripRemoves unstable cc_version fingerprint from system prompt
sort-stabilizationDeterministic ordering of tool and MCP definitions
ttl-managementDetects server TTL tier, injects correct cache_control markers
identity-normalizationNormalizes message identity fields for prefix stability
fresh-session-sortFixes non-deterministic ordering on first turn
cache-control-normalizeNormalizes cache_control markers across messages
cache-telemetryExtracts cache stats from response headers → ~/.claude/quota-status/{account.json,sessions/<id>.json}
session-healthObserves per-session thinking-desync risk (context size + thinking-block count) and warns before a session reaches the danger zone. Read-only
thinking-block-sanitizeDrops omitted (empty-text) thinking blocks to head off the CC thinking-desync 400 (#63147). On by default as of v4.0.0 (v1 mode). Set CACHE_FIX_THINKING_SANITIZE=off to disable, =v2 for additional tools-hash-mismatch drop (opt-in).
workflow-agent-id-synthesisDerives a stable per-leg agent id for Workflow-tool subagents whose canonical x-claude-code-agent-id header CC does not set (CC#66761). On by default; stash lives on ctx.meta._workflowAgentId and never leaves the proxy. usage-log emits the agent_id + agent_id_source fields when CACHE_FIX_USAGE_LOG_AGENT_ID=on AND meter v0.8.0+ is installed. Master switch: CACHE_FIX_WORKFLOW_AGENT_DERIVATION=off.
session-budget-breakerOpt-in hard per-session spend ceiling — short-circuits a session's requests locally once its cumulative tokens / estimated cost / consumption rate cross a limit you set, so a runaway fan-out can't drive credits or auto-purchase (CC#68285). Default-off; fail-open. Gate CACHE_FIX_SESSION_BUDGET=on + a ceiling. See Session budget circuit breaker.

Extensions live as .mjs files in proxy/extensions/ with configuration in proxy/extensions.json. As of v4.0.0 the proxy loads them once at startup; adding, removing, or modifying an extension requires a supervisor-level proxy restart (see Upgrading from v3.x). Hot-reload is available as opt-in via CACHE_FIX_HOT_RELOAD=on for users who want the v3.x behavior back; that path is subject to the Node ESM stale-import race documented in #196.

Developing a new extension? See docs/parallel-proxy-test-harness.md for the pattern we use to test extensions end-to-end against real claude -p traffic without disturbing the production proxy.

Running as a service

Recommended (Linux/macOS) — install-service subcommand:

cache-fix-proxy install-service

Detects your platform and writes the appropriate config:

  • Linux~/.config/systemd/user/cache-fix-proxy.service (systemd user unit)
  • macOS~/Library/LaunchAgents/com.cnighswonger.cache-fix-proxy.plist (launchd agent)

The output prints the next-step commands to enable and start the service. On Linux:

systemctl --user daemon-reload
systemctl --user enable --now cache-fix-proxy
systemctl --user enable --now cache-fix-proxy-healthcheck.timer   # auto-recovery — see below
sudo loginctl enable-linger $USER   # optional: start on boot, not just on login

Auto-recovery (Linux): install-service also drops a healthcheck companion (cache-fix-proxy-healthcheck.service + .timer). The timer fires every 2 minutes; the oneshot service runs curl -fs http://127.0.0.1:<port>/health and systemctl --user start cache-fix-proxy.service if the probe fails. This recovers the proxy from any stop — clean or unclean, expected or unexpected — within 2 minutes. Background: Restart=on-failure doesn't fire on clean stops, so before this companion existed, a systemctl stop from any source (including unidentified ones during an Anthropic outage on 2026-04-25) would leave the proxy down indefinitely. macOS doesn't need the companion — launchd's KeepAlive already auto-restarts on any exit.

On macOS:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.cnighswonger.cache-fix-proxy.plist
launchctl enable gui/$(id -u)/com.cnighswonger.cache-fix-proxy
launchctl kickstart gui/$(id -u)/com.cnighswonger.cache-fix-proxy

The installed config picks up CACHE_FIX_PROXY_PORT, CACHE_FIX_PROXY_UPSTREAM, and CACHE_FIX_DEBUG from the env at install time. Re-run install-service --force to regenerate after env changes, or edit the service file directly. Pair with cache-fix-proxy uninstall-service to remove cleanly (stops, disables, deletes).

The service runs cache-fix-proxy server in the foreground, which is just the proxy without the wrapper-mode claude launcher.

Manual (any platform):

nohup cache-fix-proxy server > /tmp/cache-fix-proxy.log 2>&1 &
echo 'export ANTHROPIC_BASE_URL=http://127.0.0.1:9801' >> ~/.bashrc

Docker

A multi-arch (amd64, arm64) container image is published to GitHub Container Registry on every release tag.

docker run -d --name cache-fix-proxy \
  --restart=always \
  -p 9801:9801 \
  ghcr.io/cnighswonger/claude-code-cache-fix:latest

# Then in your shell:
export ANTHROPIC_BASE_URL=http://127.0.0.1:9801

Use --restart=always instead of the systemd healthcheck companion — Docker handles auto-recovery natively. Mount nothing; the container is stateless. Override the default port with -e CACHE_FIX_PROXY_PORT=.... Override the upstream (e.g. to chain through llm-relay) with -e CACHE_FIX_PROXY_UPSTREAM=http://host.docker.internal:8080. The image runs as the unprivileged node user (uid 1000) and exposes a HEALTHCHECK Docker can use for liveness.

For corporate environments behind an SSL-inspecting proxy, mount your CA bundle and set the env vars:

docker run -d --name cache-fix-proxy --restart=always -p 9801:9801 \
  -e HTTPS_PROXY=http://proxy.corp.example:8080 \
  -e CACHE_FIX_PROXY_CA_FILE=/etc/ssl/corp-ca.pem \
  -v /path/to/zscaler-root.pem:/etc/ssl/corp-ca.pem:ro \
  ghcr.io/cnighswonger/claude-code-cache-fix:latest

Image tags: latest, 4, 4.0, 4.0.0 (semver-ladder, so 4 always points to the newest 4.x). latest always tracks the newest tagged release.

Linux note: the chained-upstream host.docker.internal example below is automatic on Docker Desktop (macOS / Windows). On plain Linux Docker Engine you usually need --add-host=host.docker.internal:host-gateway so the name resolves to the host bridge. Without it, the container's name lookup fails and the proxy can't reach the upstream service running on the host. Example chaining cache-fix proxy through llm-relay running on the host:

docker run -d --name cache-fix-proxy --restart=always -p 9801:9801 \
  --add-host=host.docker.internal:host-gateway \
  -e CACHE_FIX_PROXY_UPSTREAM=http://host.docker.internal:8080 \
  ghcr.io/cnighswonger/claude-code-cache-fix:latest

Forward-proxy mode in Docker (keeps Remote Control; see Forward-proxy mode). Add -e CACHE_FIX_FORWARD_PROXY=on and point CACHE_FIX_CA_DIR at a writable path. The image runs as the unprivileged node user (uid 1000), and a fresh Docker named volume mounts root-owned, so use a bind mount you chown to uid 1000 (this also persists the CA across restarts and lets the host read it):

mkdir -p ./cache-fix-ca && sudo chown 1000:1000 ./cache-fix-ca
docker run -d --name cache-fix-proxy --restart=always -p 9801:9801 \
  -e CACHE_FIX_FORWARD_PROXY=on \
  -e CACHE_FIX_CA_DIR=/ca -v "$PWD/cache-fix-ca:/ca" \
  ghcr.io/cnighswonger/claude-code-cache-fix:latest

# The CA is now at ./cache-fix-ca/ca.pem on the host. Point the client at the
# proxy (leave ANTHROPIC_BASE_URL unset so Remote Control stays enabled):
HTTPS_PROXY=http://127.0.0.1:9801 NODE_EXTRA_CA_CERTS=$PWD/cache-fix-ca/ca.pem claude

If you don't need the CA to persist on the host, drop the volume and let it live in the container's writable layer: -e CACHE_FIX_CA_DIR=/tmp/cache-fix-ca (then docker cp cache-fix-proxy:/tmp/cache-fix-ca/ca.pem ./ca.pem to fetch it). Check it worked: curl -s localhost:9801/health must report "forward_proxy":true; a false there means the proxy fell back to reverse-proxy (e.g. an unwritable CA dir).

Health check

curl http://127.0.0.1:9801/health
# {"status":"ok"}

Proxy configuration

All proxy settings are controlled via environment variables. Set them before starting the proxy server.

VariableDefaultDescription
CACHE_FIX_PROXY_PORT9801Listen port
CACHE_FIX_PROXY_BIND127.0.0.1Bind address
CACHE_FIX_PROXY_UPSTREAMhttps://api.anthropic.comUpstream URL. Change to chain another proxy (e.g. http://localhost:8080)
CACHE_FIX_FORWARD_PROXYunsetSet to on for forward-proxy mode (HTTP CONNECT + selective MITM of the upstream host) so the client points HTTPS_PROXY at the proxy instead of ANTHROPIC_BASE_URL, keeping Remote Control enabled. See Forward-proxy mode.
CACHE_FIX_CA_DIR~/.claude/cache-fix-caDirectory for the forward-proxy CA/leaf cert (generated once on first start). The client trusts ca.pem via NODE_EXTRA_CA_CERTS.
CACHE_FIX_PROXY_TIMEOUT600000Request timeout in milliseconds
CACHE_FIX_EXTENSIONS_DIRproxy/extensions/Directory for extension .mjs files
CACHE_FIX_EXTENSIONS_CONFIGproxy/extensions.jsonExtension configuration file
CACHE_FIX_DEBUG0Enable debug logging
CACHE_FIX_HOT_RELOADunsetSet to on to enable in-process extension hot-reload. Off by default as of v4.0.0 — see Upgrading from v3.x for details and the supervisor restart flow.
CACHE_FIX_READ_DEDUPEunsetSet to 1 to dedupe repeat Read tool results that re-appear unchanged across turns. Keeps the first occurrence intact; replaces later byte-identical ones (keyed on file_path + content + offset + limit) with a stable pointer line. Default-off; opt in per session to validate before broader rollout. See extension impact guide.
CACHE_FIX_ADVISOR_PLANunsetPlan override for tools/tier-advisor.mjs — one of max-5x, max-20x, pro. Bypasses heuristic plan detection. See Tier advisor.
CACHE_FIX_ADVISOR_UPGRADE_THRESHOLD80Projected-Q7d percent that triggers an UPGRADE recommendation from tier-advisor.
CACHE_FIX_ADVISOR_DOWNGRADE_THRESHOLD20Projected-Q7d percent that triggers a DOWNGRADE recommendation from tier-advisor (paired with the DOWNGRADE_WEEKS consecutive-weeks gate).
CACHE_FIX_ADVISOR_DOWNGRADE_WEEKS2Consecutive completed weeks under the downgrade threshold required before tier-advisor recommends downgrade. Single-week dips never trigger; single-week spikes DO trigger upgrade (cost-of-being-throttled asymmetry).

Corporate environments (proxies, custom CAs)

The proxy honors the following environment variables when forwarding to api.anthropic.com. Behind Zscaler / Netskope / Forcepoint / Bluecoat / corporate squid, set these in the proxy's environment.

VariableEffect
HTTPS_PROXY / HTTP_PROXY (and lowercase variants)Routes upstream requests through the corporate HTTP CONNECT proxy.
NO_PROXYComma-separated host list to bypass the proxy. Supports * and .suffix.example.com.
CACHE_FIX_PROXY_CA_FILEPath to a PEM file with one or more extra CA certificates (for SSL-inspecting proxies).
NODE_EXTRA_CA_CERTSStandard Node mechanism — also honored.
CACHE_FIX_PROXY_REJECT_UNAUTHORIZED=0Insecure escape hatch. Disables TLS verification. Use only as a last resort while you wait for IT to provide the corp CA bundle.

Example (Windows PowerShell):

$env:HTTPS_PROXY = 'http://proxy.corp.example:8080'
$env:NO_PROXY    = 'localhost,127.0.0.1,.corp.example'
$env:CACHE_FIX_PROXY_CA_FILE = 'C:\corp\zscaler-root.pem'
node "$(npm root -g)\claude-code-cache-fix\proxy\server.mjs"

Stderr will print [upstream] using proxy http://proxy.corp.example:8080 ... on first request when the agent is wired correctly. With no proxy/CA env vars set, behavior is unchanged from earlier versions (Node default agent, system trust store).

Embedding the proxy in your own process

If you ship a Node or Bun binary that wants the cache-fix proxy in-process (e.g. a Bun-compiled agent that avoids forking a Node child), import the factory from claude-code-cache-fix/proxy/server:

import { startProxy } from "claude-code-cache-fix/proxy/server";

const handle = await startProxy({
  port: 0,        // OS-assigned ephemeral port; pass a number to pin
  bind: "127.0.0.1",
  watch: false,   // skip fs.watch — recommended for compiled binaries
});

console.log(`proxy listening on ${handle.address}:${handle.port}`);

// ...later...
await handle.close();

createProxyServer()http.Server builds the request handler wired into an http.Server. The returned server is not listening and the extension pipeline has not been loaded — use this when you want to manage the lifecycle yourself.

startProxy(options?)Promise<{ server, port, address, close }> loads the extension pipeline, optionally starts the file watcher, and starts listening. Returns a handle with the bound port (resolved when port: 0 is requested) and a close() that releases the server and the watcher.

Options (all optional; all fall back to the same env vars used by the CLI):

OptionDefaultEffect
portCACHE_FIX_PROXY_PORT env, else 9801Listen port. Pass 0 for an OS-assigned ephemeral port.
bindCACHE_FIX_PROXY_BIND env, else 127.0.0.1Bind address.
extensionsDirpackage proxy/extensions/Directory to load .mjs extensions from.
extensionsConfigpackage proxy/extensions.jsonPath to extension config.
watchtrueWhether to start fs.watch on the extensions config. Set false for embedded / compiled-binary use.

One extension registry per process. The pipeline maintains a single shared extension registry at module scope. Hosting two startProxy() instances in the same process is supported (different ports, different bind addresses), but they share that registry — a subsequent loadExtensions call replaces it for both. If you need divergent extension configs per instance, run them in separate processes.

CLI invocation is unchanged. node proxy/server.mjs, cache-fix-proxy server, and the wrapper's child-fork path all auto-listen and install SIGTERM/SIGINT handlers as before. Library imports never trigger that behavior — the auto-listen is gated behind a main-module check.

The embeddable factory was contributed by @bilby91 at Crunchloop DAP — see PR #123.

Upgrading from v3.x

Behavior changes in v4.0.0:

  • thinking-block-sanitize v1 is now on by default. Was opt-in via CACHE_FIX_THINKING_SANITIZE=on in v3.8.0–v3.9.x. After seven days of prod dogfood across 37 sessions (zero cannot be modified 400s, cache hit-rate aggregate 94.66% vs. 92.44% baseline, sanitize firing on ~35% of sessions with ~800 blocks dropped per day) the v1 mitigation is the new default. Set CACHE_FIX_THINKING_SANITIZE=off to explicitly disable. v2 (additional tools-hash-mismatch drop) stays opt-in via =v2. See #63147 and #162.
  • In-process extension hot-reload is now off by default. Was on in v3.x. Set CACHE_FIX_HOT_RELOAD=on to restore the prior behavior. Off-by-default eliminates the Node ESM stale-import race documented in #196, where the watcher silently failed to load a newly-merged extension for 17 hours after a hot-reload trigger. The race fires when the file watcher re-imports an extension whose transitive dependencies are already cached by Node's loader; cold starts are unaffected.

Embedder note (Bun hosts, DAP-style integrations using createProxyServer() / startProxy())

v4.0.0 flips CACHE_FIX_THINKING_SANITIZE from default-off to default-on. The v1 omitted-text drop will run on every request body passing through the embedded proxy. If your host depends on the prior no-sanitization behavior (e.g., your downstream code expects empty thinking blocks to survive the proxy round-trip), preserve it by either:

  • Setting CACHE_FIX_THINKING_SANITIZE=off in your host's environment, OR
  • Setting process.env.CACHE_FIX_THINKING_SANITIZE = "off" in your code at any point before request handling — the mode is read per-request via modeFromEnv(), not cached at module load.

The flip is backed by 7 days of prod dogfood (37 sessions, zero cannot be modified 400s, cache hit-rate aggregate 94.66% vs 92.44% baseline). See PR #201 for the validation data and #63147 for upstream context.

Picking up a new extension or a code change to an existing one in v4.0.0 requires a supervisor-level proxy restart. There are two upgrade flows depending on whether you also want to opt back into hot-reload.

Flow 1 — code-only npm upgrade (recommended default)

Your existing systemd unit / launchd plist is unchanged; only the proxy code on disk is updated by npm. Restart the running process to pick up the new code.

Linux (systemd user unit):

npm install -g claude-code-cache-fix@4
systemctl --user restart cache-fix-proxy

No daemon-reload required — the unit file content is unchanged.

macOS (launchd user agent):

npm install -g claude-code-cache-fix@4
launchctl kickstart gui/$(id -u)/com.cnighswonger.cache-fix-proxy

kickstart re-execs the agent under the existing plist.

Flow 2 — opt back into hot-reload at the supervisor layer

Run if you actively use hot-reload (e.g., you drop custom extensions into the extensions dir on a live proxy and want them picked up without restart). This rewrites the unit / plist so CACHE_FIX_HOT_RELOAD=on is set every time the supervisor starts the proxy.

Linux (systemd user unit):

CACHE_FIX_HOT_RELOAD=on cache-fix-proxy install-service
systemctl --user daemon-reload
systemctl --user restart cache-fix-proxy

daemon-reload is required because the unit file content changed.

macOS (launchd user agent):

CACHE_FIX_HOT_RELOAD=on cache-fix-proxy install-service
launchctl bootout gui/$(id -u)/com.cnighswonger.cache-fix-proxy
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.cnighswonger.cache-fix-proxy.plist
launchctl kickstart gui/$(id -u)/com.cnighswonger.cache-fix-proxy

bootout + bootstrap is required because the plist contents changed — kickstart alone does not pick up plist changes.

Note on the hot-reload tradeoff: even on the opt-in path, the ESM stale-import race remains possible on long-running processes. If you hit a degraded /health (returns 503 + {status:"degraded",...}), a process restart is the only recovery; the proxy logs a [CRITICAL] hint when this happens. See #197 for the observability layer.

What this proxy defends against

Cache-economics regressions. The original purpose of cache-fix is to absorb the cache-handling behaviors in Claude Code that cost users real money and quota — TTL downgrades, cache-breaking header churn, identity-latching issues, and the rest of the regression catalog documented across our issue history. The proxy sits between CC and the Anthropic API, normalizes the request and response stream, and emits enough observability (via statusline integration and the quota-status files) that users can see what their session is actually doing. This is the load-bearing feature for almost every user today.

Bootstrap-channel observability. Claude Code v2.1.150 introduced a prompt-section consumer that fetches a server-supplied string from /api/claude_cli/bootstrap and merges it into the agent's behavioral-instructions prompt path. We filed this behavior with Anthropic's security team in May 2026; Anthropic closed the report as Informative, treating TLS as the transport-integrity boundary and declining to add application-layer authenticity checks. Cache-fix shipped explicit handling for this path in v3.7.0 and extended it in v3.7.1 to also cover the env-var-selected GrowthBook prompt-injection surface that landed in CC v2.1.152 (remote-control mode: CLAUDE_CODE_SYSTEM_PROMPT_GB_FEATURE names a flag key whose cached value is used as the system prompt body). Stable in the current v4.x line.

Cache-fix's bootstrap-defense extension ships three modes, selected via CACHE_FIX_BOOTSTRAP_MODE:

ModeDefault?Behavior
audityesBootstrap responses proxy through to CC. Each response is logged to ~/.claude/cache-fix-bootstrap-log.jsonl with surface metadata: which prompt-source surfaces fired (tengu_heron_brook legacy and/or env-var-selected), the SHA-256 hash of the value (first 16 hex chars — never the value itself), and the CLAUDE_CODE_REMOTE flag. Multi-surface responses emit one record per surface, correlated by request_id + timestamp window.
blockopt-inonRequest returns a 200 with an empty JSON body. Upstream is never called, no flag map ever reaches the on-disk GrowthBook cache. Defeats both legacy and env-var-selected injection surfaces.
allowlistopt-in (experimental)Bootstrap response proxies through, but prompt-source-eligible keys (legacy tengu_heron_brook + env-var-selected key) not in the allowlist are stripped from the response body before it reaches CC. Default allowlist is tengu_heron_brook (the only known-legitimate historical key); configure via CACHE_FIX_BOOTSTRAP_ALLOWED_KEYS=comma,separated,list. Pass CACHE_FIX_BOOTSTRAP_ALLOWED_KEYS= (explicit empty) for full deny-all. Other GrowthBook flag keys pass through untouched. May need updates if Anthropic adds legitimate prompt-source keys in future CC releases.

Note: cache-fix v3.6.2 and earlier returned 404 for the bootstrap path because the proxy router did not include it — the practical effect was that bootstrap content was not reaching CC for cache-fix users. v3.7.0's default audit changes that behavior; explicit CACHE_FIX_BOOTSTRAP_MODE=block preserves it. The full disclosure record, including Anthropic's verbatim close text, is in docs/disclosure/heron-brook-2026-05.md.

Reference material:

Auto-1M-context overage protection. CC v2.1.161 onward (notably the VS Code Extension surface) can auto-select 1M context on Pro Plan without user request, immediately consuming overage credits. The proxy's auto-1m-guard extension detects the context-1m-2025-08-07 token on the outbound anthropic-beta header and either warns or strips it, depending on the mode you opt into via CACHE_FIX_AUTO_1M_GUARD:

ModeDefault?Behavior
offnoExtension no-op.
warnyesDetect the token. Stash an annotation into the per-session JSON (auto_1m_detected, auto_1m_action: "warn", auto_1m_advice) and emit a stderr log line. Does not modify the request.
stripopt-inDetect AND remove the token from the anthropic-beta header before forwarding. Annotation: auto_1m_action: "stripped".

The CC-side kill switch is CLAUDE_CODE_DISABLE_1M_CONTEXT=1 (env var), which is the right fix when it actually reaches the CC process. On the VS Code extension surface that env var is reportedly unreliable; the proxy intercept bypasses that gap because it acts on the wire regardless of which CC launcher produced the request. Tracks CC#64919; see docs/directives/proxy-auto-1m-guard.md for the binary-walk that confirms the proxy-visible signal is the beta header (CC strips the [1m] suffix from req.body.model client-side before sending).

Client-side hooks

Some Claude Code behaviors live below the request layer — they happen client-side, in the tool-dispatch path, before the proxy ever sees traffic. cache-fix ships standalone hook scripts under hooks/examples/ for those cases. They're independent of the proxy and you install them by pointing at them from your own ~/.claude/settings.json.

ScriptWhat it does
worktree-edit-guard.pyBlock Edit/Write/MultiEdit/NotebookEdit tool calls whose target path escapes the active git worktree, preventing parent-checkout corruption from worktree sessions. Addresses CC#59628.

Contributed tools

Standalone scripts that aren't proxy extensions or CC hooks — installable separately, addressing specific upstream issues.

ToolWhat it does
tools/gh-auth-status-shim/PATH-resolved gh wrapper that suppresses CC Desktop's false "GitHub CLI authentication expired" toast. Addresses CC#67055: CC Desktop's PR poller maps any non-zero return from gh auth status (including its 5s spawn timeout) to the "auth" toast category. The shim intercepts gh auth status calls with a 4s internal timeout, classifies the outcome, and returns exit 0 to suppress the false toast on transient/timeout signals while letting genuine expiry (not logged in, HTTP 401) propagate normally. Workaround until Anthropic's classifier fix lands. Known limitations: rewrites gh auth status exit-code semantics for every caller in the PATH scope (not just CC); macOS coverage unverified due to launchd PATH inheritance; native Windows CC Desktop not supported.

Recommended CC operational config

The proxy fixes what it can fix at the request layer. A handful of CC client-side env vars and ~/.claude/settings.json knobs solve adjacent problems the proxy can't reach — silent model swaps on CC update, ambiguous model fallback, schema-strip side effects. Surfacing these here as a recommendation; users decide their own config.

These findings come from @fgrosswig's binary analysis of CC v2.1.91. Methodology is public PowerShell + ASCII string extraction; he shared the resulting punch list privately as a courtesy.

Suggested ~/.claude/settings.json env block

The model IDs below are illustrative — replace with your preferred main and small-fast models. The point is that pinning something explicit beats relying on CC's defaults.

{
  "env": {
    "CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP": "1",
    "ANTHROPIC_MODEL": "claude-opus-4-7",
    "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5-20251001"
  }
}

CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP=1 — single most impactful flag. CC has a legacy code path that silently remaps your pinned model to a different one after certain version updates. Setting this to 1 disables the remap; the model you pin is the model you get. (If you don't pin, CC's defaults apply as usual.)

ANTHROPIC_MODEL — pins the primary model. Keeping this explicit means the cache prefix hash stays stable across CC version bumps that would otherwise swap your default. Adjust to whichever model you actually want.

ANTHROPIC_SMALL_FAST_MODEL — pins the side-channel "fast" model CC uses for short auxiliary calls (e.g., title generation, classification). Without an explicit pin, this can silently fall back to a different family on update.

autoCompactWindow=1000000 caveat

If you've seen the autoCompactWindow: 1000000 setting recommended elsewhere: it only takes effect when the active model qualifies for 1M-context (currently claude-sonnet-4-6 or claude-opus-4-6 with the appropriate beta header). Without those preconditions it caps at the hardcoded 200K regardless of what you set.

CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 schema-strip side effect

If you set this flag, CC strips any tool field outside ["name", "description", "input_schema", "cache_control"] from outgoing requests. Custom tools relying on defer_loading or eager_input_streaming will silently lose those fields and behave differently. Worth knowing before turning the flag on.

Known CC behaviors that affect cache cost

These aren't bugs cache-fix patches — they're upstream CC behaviors users should be aware of when sizing their session cost.

Diagnostic slash commands inflate conversation history (#49335)

Running /context, /release-notes (and likely other state-inspection commands) appends the diagnostic output to conversation history rather than rendering terminal-only. Subsequent turns replay the inflated payload via prompt cache, compounding token cost on a state-inspection action that should be free. Empirically measured at +3,480 cache_creation_input_tokens for a single /context invocation on v2.1.148; another user reports ~5K on a separate session. /release-notes is worse — defaults to dumping the full changelog.

Worse for diagnosis: the inflated payload that bills against your cache isn't written to the local JSONL transcript, so you can't audit the cost source locally — you can only infer it from cache_creation_input_tokens jumps in response usage metadata. (Proxy-mode users can inspect the deltas in ~/.claude/quota-status/ files, which the proxy writes directly from response headers.)

Workaround until upstream fix: use these commands sparingly in long sessions. If you need them frequently in a session, consider /compact after a diagnostic run to reset the bleed.

Quick Start: Preload (CC v2.1.112 and earlier)

If you're on a Node.js-based CC version (v2.1.112 or earlier), the preload interceptor works without a proxy:

npm install -g claude-code-cache-fix
NODE_OPTIONS="--import claude-code-cache-fix" claude

Note: The preload does NOT work on CC v2.1.113+ (Bun binary). Use the proxy above.

See docs/preload-setup.md for wrapper scripts, shell aliases, Windows instructions, and VS Code preload-mode integration.

VS Code Extension

The VS Code extension (v0.5.0) supports both proxy and preload modes:

Proxy mode (recommended):

  1. Start the proxy (see above)
  2. In VS Code command palette: Claude Code Cache Fix: Enable Proxy Mode
  3. Restart any active Claude Code session

Preload mode (CC ≤v2.1.112):

  1. npm install -g claude-code-cache-fix
  2. Download the VSIX from GitHub Releases
  3. Install: code --install-extension claude-code-cache-fix-0.5.0.vsix
  4. Command palette: Claude Code Cache Fix: Enable

For manual VS Code wrapper setup (without the VSIX), see docs/preload-setup.md.

Security model

The proxy and interceptor have full read/write access to API requests and responses. This is inherent to the approach — any fetch interceptor, proxy, or gateway has this position.

What it does: Modifies outgoing request structure (block order, fingerprint, TTL, git-status) to fix cache bugs. Reads response headers and SSE usage data for monitoring.

What it does NOT do: No network calls from the proxy or interceptor. All telemetry is written to local files under ~/.claude/. No data leaves your machine.

Supply chain: Proxy mode: small focused extension modules in proxy/extensions/ (most under a few hundred lines; the pipeline is composable, you can read any single one in isolation). Preload mode: single unminified file (preload.mjs). One dev dependency (zod for schema validation in tests only). Review before installing. Published builds carry npm's default registry signatures; sigstore provenance attestation is not currently published — tracked as a follow-up.

Independent audit: Assessed as "LEGITIMATE TOOL" by @TheAuditorTool (2026-04-14).

The problem

When you use --resume or /resume in Claude Code, the prompt cache breaks silently. Instead of reading cached tokens (cheap), the API rebuilds them from scratch on every turn (expensive). A session that should cost ~$0.50/hour can burn through $5–10/hour with no visible indication anything is wrong.

Three bugs cause this:

  1. Partial block scatter — Attachment blocks (skills listing, MCP servers, deferred tools, hooks) are supposed to live in messages[0]. On resume, some or all drift to later messages, changing the cache prefix.

  2. Fingerprint instability — The cc_version fingerprint (e.g. 2.1.92.a3f) is computed from messages[0] content including meta/attachment blocks. When those blocks shift, the fingerprint changes, the system prompt changes, and cache busts.

  3. Non-deterministic tool ordering — Tool definitions can arrive in different orders between turns, changing request bytes and invalidating the cache key.

Additionally, images read via the Read tool persist as base64 in conversation history and are sent on every subsequent API call, compounding token costs silently.

How it works

Proxy mode (v3.0.0+): An HTTP server on localhost:9801 intercepts POST /v1/messages requests. A pipeline of extension modules processes each request — normalizing block order, stripping fingerprints, stabilizing tool sort, managing TTL markers, sanitizing thinking blocks, recording telemetry, and more. Extensions live as .mjs files configured in proxy/extensions.json and load once at proxy startup (hot-reload is opt-in as of v4.0.0 — see Upgrading from v3.x). All other traffic passes through untouched.

Preload mode (v2.x): A Node.js --import module that patches globalThis.fetch before Claude Code makes API calls. Applies the same fixes inline — scans user messages for relocated blocks, sorts tools, recomputes fingerprints, injects TTL markers.

Both modes are idempotent — if nothing needs fixing, the request passes through unmodified. Neither mode modifies your conversation; they only normalize the request structure before it hits the API.

Graduating from fixes

The package serves three purposes with different lifecycles:

PurposeExamplesWhen to disable
Bug fixesBlock relocation, fingerprint, tool sort, TTLWhen CC fixes the underlying bug — check the health line
MonitoringQuota tracking, microcompact detection, GrowthBook flagsKeep permanently — these detect future regressions
OptimizationsImage stripping, output efficiency rewriteKeep as long as they help your workflow

Health status (preload mode)

On first API call, the interceptor logs a health status line (requires CACHE_FIX_DEBUG=1):

cache-fix health: relocate=active(2h ago) fingerprint=dormant(5 clean sessions) tool_sort=active ttl=active identity=waiting
  • active(Xh ago) — fix was applied recently
  • dormant(N clean sessions) — bug not detected in N sessions; CC may have fixed it
  • safety-blocked(Nx) — round-trip verification failed; fix auto-disabled
  • waiting — fix hasn't been triggered yet

Regression detection

If cache_read ratio drops below 50% across 5+ calls after disabling fixes:

REGRESSION WARNING: cache_read ratio averaged 12% across last 5 calls.
Fixes are disabled — consider re-enabling to recover cache performance.

Safety

Fingerprint round-trip verification

Before rewriting the cc_version fingerprint, the interceptor verifies that its hardcoded salt and character indices reproduce the fingerprint Claude Code sent. If verification fails (CC changed its algorithm), the rewrite is skipped automatically. This ensures the interceptor can never make cache performance worse than stock CC.

Fail-safe design

Every fix is designed to fail to a no-op:

  • If block detection regexes don't match → blocks aren't relocated (CC behavior)
  • If fingerprint format changes → fingerprint isn't rewritten (CC behavior)
  • If tool sort produces no changes → payload passes through untouched
  • If TTL injection target structure changes → TTL isn't injected (CC behavior)

The interceptor can only help or do nothing. It cannot make things worse.

Status line — quota warnings in real time

Both modes write quota state on every API call. Proxy mode (v3.5.0+) splits into ~/.claude/quota-status/account.json (account-global fields: Q5h/Q7d, status, overage) plus ~/.claude/quota-status/sessions/<id>.json (per-session cache fields: TTL tier, hit rate). Preload mode keeps the legacy ~/.claude/quota-status.json (single-session by construction). The included tools/quota-statusline.sh script displays a live status line showing:

  • Q5h quota bar [███░┃░░░░░] + percent + (exhaust X, reset Y). Filled cells are consumed quota; the heavy-vertical tick is wall-clock elapsed position in the window. Tick to the right of the fill = under pace; tick inside the fill = burning faster than time (over pace). exhaust is the projected time-to-100% at the current burn rate; reset is the wall-clock time until the window rolls over. When exhaust < reset, you will hit 100% before the window resets — back off.
  • Q7d same shape with day-scale durations (e.g. (exhaust 3d13h, reset 3d0h)). Below a day, the suffix auto-switches to h/m format (e.g. (exhaust 1h41m, reset 0h30m)).
  • TTL tierTTL:1h when healthy, TTL:5m in red when the server has downgraded you (typically at Q5h ≥ 100%)
  • PEAK in yellow during weekday peak hours (13:00–19:00 UTC)
  • Cache hit rate %
  • OVERAGE flag when active
  • Served-model divergence indicator — when the served model differs from the requested model (the classifier-driven swap pattern in CC#66728), the bar gains a red requested → served segment, or a black-on-yellow requested → served for sticky state once the family-aware heuristic latches. No segment appears on the default no-divergence path. [1m] suffix appears on the requested side only when auto_1m_detected is set.

Example line (mid-window, healthy state):

Q5h [███░┃░░░░░] 30% (exhaust 4h40m, reset 3h00m) | Q7d [█████┃░░░░] 53% (exhaust 3d13h, reset 3d0h) | TTL:1h 98.3%

The (exhaust …, reset …) suffix is dropped piecewise when projection isn't meaningful: at 0% (fresh window) and 100% (already exhausted) only reset is shown; in the first 5 minutes after window start the burn rate isn't stable enough to project (a single early call dominates the rate), so exhaust is held back until then on both Q5h and Q7d; a stale resets_at (the server-reported value sits in the past, before the next API call refreshes it) drops both.

The bar uses Unicode block characters (█┃░) — most modern terminals render these correctly. If your terminal substitutes boxes or replacement glyphs, configure a Unicode-capable font (any DejaVu, Fira, Iosevka, JetBrains Mono, etc.).

Setup

mkdir -p ~/.claude/hooks
cp "$(npm root -g)/claude-code-cache-fix/tools/quota-statusline.sh" ~/.claude/hooks/
chmod +x ~/.claude/hooks/quota-statusline.sh

Add to ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/hooks/quota-statusline.sh"
  }
}

Why the status line matters

When the server downgrades your TTL to 5m (quota-aware downgrade at Q5h ≥ 100%), every idle longer than 5 minutes causes a full context rebuild. Without the status line, this is invisible. With it, the red TTL:5m warning tells you: stop working, wait for the Q5h window to reset, then resume. Powering through overage compounds the drain; pausing breaks the cycle.

Recommended: disable git-status injection

Claude Code injects live git status into the system prompt on every call. Any file edit changes the git status, which busts the entire prefix cache. Disabling this saves ~1,800 tokens per call:

export CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1

Or add "includeGitInstructions": false to ~/.claude/settings.json. Claude Code can still run git status via the Bash tool when it needs context. Community-validated by @wadabum: 18-token cache creation across git state changes (vs thousands without the flag).

Why we don't ship a proxy extension for this: the proxy intercepts requests after Claude Code has already composed the system prompt — by then the volatile git status text is already part of the prefix that the model conditioned on in the previous turn, and stripping it post-hoc would itself bust the cache. The fix has to happen at the source. CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1 prevents the injection before the prompt is composed, which is why the native flag is the right tool. Stripping post-hoc would also remove model-visible context that an explicit Bash call can recover, and would risk false-positive matches against assistant-written text.

Migration: v3.4.x → v3.5.0+

If you wrote a custom statusline, monitoring script, or anything else that reads ~/.claude/quota-status.json directly, this section is for you. v3.5.0 split that file in proxy mode; preload mode is unchanged.

What changed

v3.4.x and earlier (proxy + preload)v3.5.0+ proxy modev3.5.0+ preload mode
Quota fields (Q5h, Q7d, status, overage)~/.claude/quota-status.json~/.claude/quota-status/account.json~/.claude/quota-status.json (legacy path)
Cache fields (TTL tier, hit rate, cache_creation/read)same file as above~/.claude/quota-status/sessions/<filename>.jsonsame file as above
Multi-session attributionnone — last writer winsper-session filespreload is single-session by construction

<filename> is derived from the request's x-claude-code-session-id header via a deterministic safe-name rule: UUIDs and other ids matching [A-Za-z0-9_-]{1,128} pass through; null/empty/whitespace become unknown; anything else is mapped to inv-<sha256-prefix>. Full rule is documented at docs/directives/proxy-quota-status-per-session.md.

The legacy ~/.claude/quota-status.json is auto-deleted on the first proxy-mode write after upgrade. Per-session files older than CACHE_FIX_QUOTA_STATUS_TTL_DAYS (default 7) are swept on write.

Consumer-side migration pattern

Your script should try the v3.5.0+ proxy paths first and fall back to the legacy path if not present. That way it works in both modes (and on hosts mid-upgrade). The session id usually comes from Claude Code's stdin when it invokes a statusline hook; for other consumers, capture it from the most-recently-modified ~/.claude/projects/*/*.jsonl filename.

Bash (statusline-style):

QS_DIR="$HOME/.claude/quota-status"
ACCOUNT="$QS_DIR/account.json"
LEGACY="$HOME/.claude/quota-status.json"

# Canonical filename rule — must mirror proxy/extensions/cache-telemetry.mjs
# sessionFilename(): trim, then "" → unknown, safe regex passthrough, else
# inv-<sha256-prefix>. Without this, malformed or whitespace ids miss the
# per-session file even though the writer created one under the canonical name.
session_filename() {
  local trimmed
  trimmed="$(printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
  if [ -z "$trimmed" ]; then echo unknown; return; fi
  if printf '%s' "$trimmed" | grep -qE '^[A-Za-z0-9_-]{1,128}$'; then
    printf '%s' "$trimmed"
  else
    # sha256sum on Linux; shasum -a 256 on macOS. Both emit "<hex>  -".
    local hash
    if command -v sha256sum >/dev/null 2>&1; then
      hash="$(printf '%s' "$trimmed" | sha256sum)"
    else
      hash="$(printf '%s' "$trimmed" | shasum -a 256)"
    fi
    printf 'inv-%s' "$(printf '%s' "$hash" | cut -c1-16)"
  fi
}

# session id: prefer CC stdin, fall back to most-recent jsonl
sid="$(jq -r '.session_id // empty' 2>/dev/null < /dev/stdin || true)"
if [ -z "$sid" ]; then
  sid="$(ls -t "$HOME"/.claude/projects/*/*.jsonl 2>/dev/null | head -1 | xargs -I{} basename {} .jsonl)"
fi
filename="$(session_filename "$sid")"

# quota: account.json (v3.5.0+) → fall back to legacy
if [ -f "$ACCOUNT" ]; then
  quota_json="$(cat "$ACCOUNT")"
elif [ -f "$LEGACY" ]; then
  quota_json="$(cat "$LEGACY")"
fi

# cache: sessions/<filename>.json (v3.5.0+) → fall back to legacy
if [ -f "$QS_DIR/sessions/$filename.json" ]; then
  cache_json="$(cat "$QS_DIR/sessions/$filename.json")"
elif [ -f "$LEGACY" ]; then
  cache_json="$(cat "$LEGACY")"
fi

Node:

import { readFileSync, existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { createHash } from "node:crypto";

const home = homedir();
const accountPath = join(home, ".claude", "quota-status", "account.json");
const legacyPath = join(home, ".claude", "quota-status.json");

const SAFE_NAME_RE = /^[A-Za-z0-9_-]{1,128}$/;

// Mirror of cache-telemetry.mjs sessionFilename(). Reader-side rule must match
// writer-side rule; otherwise malformed/whitespace ids miss their per-session file.
function sessionFilename(rawId) {
  if (rawId === null || rawId === undefined) return "unknown";
  const s = String(rawId).trim();
  if (s.length === 0) return "unknown";
  if (SAFE_NAME_RE.test(s)) return s;
  return "inv-" + createHash("sha256").update(s).digest("hex").slice(0, 16);
}

function readQuotaJson() {
  if (existsSync(accountPath)) return JSON.parse(readFileSync(accountPath, "utf8"));
  if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
  return null;
}

function readCacheJson(sessionId) {
  const filename = sessionFilename(sessionId);
  const p = join(home, ".claude", "quota-status", "sessions", `${filename}.json`);
  if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
  if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
  return null;
}

The shipped tools/quota-statusline.sh is the reference implementation for the bash version. The /coffee skill v1.4.0 is the reference for the per-session warmth gate.

Why per-session

On multi-agent hosts (multiple Claude Code sessions sharing one proxy), the pre-v3.5.0 single global file caused every session to overwrite the others' cache stats with each response. A statusline reading from session A would show session B's TTL tier whenever B sent a request more recently. Per-session files plus an account-global quota file resolve this without losing the easy account-wide view. See #104 for the original report.

CLAUDE_CONFIG_DIR

Claude Code reads CLAUDE_CONFIG_DIR to relocate its config root away from the default ~/.claude (used to keep multiple independent config roots in separate directories). The proxy now honors the same variable for all of its on-disk state: quota-status/, usage.jsonl, cache-fix-state/, session mirrors, snapshots, and OAuth events all land under $CLAUDE_CONFIG_DIR instead of a hardcoded ~/.claude. When it's unset the proxy uses ~/.claude exactly as before (no change for the common single-config case).

This matters when you run one proxy per config dir: without it, every proxy writes to ~/.claude/quota-status/account.json and they clobber each other's quota state. Give each proxy the same CLAUDE_CONFIG_DIR its Claude Code client uses, and their state stays cleanly separated.

Image stripping (preload mode)

Images read via the Read tool persist as base64 in conversation history, riding along on every subsequent API call. A single 500KB image costs ~62,500 tokens per turn on Opus 4.6, and ~85,000+ on Opus 4.7 due to the new tokenizer. Image stripping is strongly recommended on 4.7.

export CACHE_FIX_IMAGE_KEEP_LAST=3

Keeps images in the last 3 user messages, replaces older ones with a text placeholder. Only targets tool_result blocks — user-pasted images are never touched.

Oversized-image guard (legacy, v3.2.1)

export CACHE_FIX_IMAGE_MAX_DIM=2000

The Anthropic API enforces TWO image-related limits on multi-image requests, and the same error message can fire for either:

"An image in the conversation exceeds the dimension limit for many-image requests (2000px). Start a new session with fewer images."

Two pressure axes to address them:

PressureVariableWhat it does
Too many images in conversationCACHE_FIX_IMAGE_KEEP_LAST=NStrips images from old user messages, keeps only the last N.
Any single image too largeCACHE_FIX_IMAGE_MAX_DIM=2000Replaces images exceeding the dimension limit with a forensic placeholder noting the original dimensions. Covers both user-message direct images and tool_result-nested images.

The two compose: with both set, KEEP_LAST runs first (drops the count), then MAX_DIM runs on what remains (caps the size of the kept ones). Common triggers for the dimension axis: hi-res manuscript scans, retina screenshots, photos at full resolution.

Pure-JS PNG and JPEG header parsing — no native deps. Other formats (GIF, WebP, AVIF, BMP) pass through unchanged regardless of dimension. Fail-open: images whose dimensions can't be parsed (truncated header, unsupported format) are kept rather than stripped — better to send a request that might error than to strip a valid image we just couldn't measure.

Image-guard pipeline (v3.3.0)

A conditional pipeline that mirrors Anthropic's actual rules. Strictly opt-in via a single env var:

export CACHE_FIX_IMAGE_GUARD=1

When enabled, the proxy runs:

PassTriggerAction
Pass 0 (legacy)CACHE_FIX_IMAGE_KEEP_LAST=N setStrip tool_result images from user messages older than N most recent
Pass 3CACHE_FIX_IMAGE_PRESERVE_DETAIL=1 AND image long edge > model native capLanczos resize via sharp to native cap (2576 px for Opus 4.7, 1568 px otherwise), preserve aspect ratio and media type
Pass 1image long edge > active rejection capStrip and replace with forensic placeholder. Active cap = MAX_DIM if set, else 2000 px (when count > 20) or 8000 px (count ≤ 20)
Pass 2request body exceeds CACHE_FIX_IMAGE_REQUEST_SIZE_MAX (default 30 MB)Drop oldest images until under budget
Count capsurviving image count > CACHE_FIX_IMAGE_COUNT_MAX (default 100)Drop oldest images down to the cap

Execution order: Pass 0 → Pass 3 → Pass 1 → Pass 2 → count cap. Each pass is independent — Pass 1 never resizes; Pass 3 never strips.

Optional sharp dependency

Pass 3 requires sharp for Lanczos resize. It's declared as an optional peer dependency — install separately if you want Pass 3:

npm install sharp

If sharp is missing, Pass 3 skips cleanly (telemetry records library_missing: true); Pass 1 + Pass 2 + the count cap still run.

Precedence matrix

Env var combinationBehavior
Nothing setNo image processing (back-compat default; the extension short-circuits).
KEEP_LAST=N onlyExisting v3.2.1: count cap on tool_result images in user messages, runs first. No pipeline.
MAX_DIM=N onlyExisting v3.2.1: hard size cap, strip-only. No pipeline.
KEEP_LAST=N + MAX_DIM=NExisting v3.2.1 composition: KEEP_LAST runs first (drops count), then MAX_DIM runs on survivors (caps size). No pipeline, no Pass 2, no Pass 3.
IMAGE_GUARD=1New pipeline: Pass 1 (conditional cap) + Pass 2 (request-size guard) + image-count cap.
IMAGE_GUARD=1 + MAX_DIM=NMAX_DIM overrides Pass 1's conditional cap (acts as the cap value); Pass 2 still runs.
IMAGE_GUARD=1 + PRESERVE_DETAIL=1Adds Pass 3 (Lanczos resize via sharp). When sharp unavailable, falls back to strip behavior.
IMAGE_GUARD=1 + KEEP_LAST=NKEEP_LAST runs first as count cap (Pass 0); pipeline runs on remainder.
IMAGE_GUARD=1 + KEEP_LAST=N + MAX_DIM=NThree-way: KEEP_LAST runs first; pipeline runs on remainder, but MAX_DIM overrides Pass 1's conditional cap; Pass 2 still runs.
PRESERVE_DETAIL=1 without IMAGE_GUARD=1Logs warning, treats as no-op. PRESERVE_DETAIL is meaningless without the pipeline running.

Tunables

Env varDefaultPurpose
CACHE_FIX_IMAGE_GUARDunsetTop-level pipeline gate (=1 enables).
CACHE_FIX_IMAGE_PRESERVE_DETAILunsetEnable Pass 3 Lanczos resize via sharp.
CACHE_FIX_IMAGE_REQUEST_SIZE_MAX31457280 (30 MB)Pass 2 byte budget. 2 MB headroom from Anthropic's 32 MB ceiling.
CACHE_FIX_IMAGE_COUNT_MAX100Hard image-count cap. Set to 600 for legacy Claude 1/2.x/Instant if needed.

Image-retry circuit breaker (proxy mode, opt-in)

When CC encounters a permanent "image could not be processed" error, the harness currently treats it as transient and retries — with full conversation context and the same 34 MB image payload — up to ~19 times per anthropics/claude-code#66815. One bad image can consume ~60% of a Max-plan user's 5-hour quota envelope before the storm naturally stops.

The breaker watches every messages-route response. When upstream returns a permanent image-processing error, it records the failure keyed by (sessionId, requestSignature) with the request's image SHA-256 hashes. When the next request on the same session carries an image whose hash matches a recorded failure within the 30-second sliding cool-off, the breaker short-circuits the retry locally — emitting a wire-format-correct synthesized response (SSE event sequence for stream:true, JSON envelope otherwise) that the harness consumes as a normal completed assistant turn. The synthesized text names the failure and asks the user to drop or replace the image. Bounds the retry storm from "many upstream calls" to one.

Opt-in via env var; default-off in v4.2.0 first ship pending sim-validation:

export CACHE_FIX_IMAGE_RETRY_BREAKER=on
ModeBehavior
onDetect + record + short-circuit retries
off (default)Pass-through, no detection, no logging
dry-runDetect + record + log JSONL events, but do not short-circuit (useful for production debugging)
Env varDefaultPurpose
CACHE_FIX_IMAGE_RETRY_BREAKERoffMode gate — on / off / dry-run
CACHE_FIX_IMAGE_RETRY_COOLOFF_MS30000Sliding cool-off window per recorded failure
CACHE_FIX_IMAGE_RETRY_MAX_ENTRIES4096LRU cap on the in-memory failure map
CACHE_FIX_IMAGE_RETRY_LOG_PATH~/.claude/image-retry-events.jsonlStructured event log path (5 MB single-tier rotation)

Observability surface: the JSONL event log is the only signal. Short-circuited requests do not produce usage.jsonl rows — they bypass usage-log and cache-telemetry entirely (no upstream call → no SSE stream → no row). Each fire writes { event: "breaker_fire", mode, session_id, image_hashes, retry_count, remaining_ms, request_id, ... }; each first-time failure writes { event: "failure_recorded", ... }. The log carries hashes and metadata only — no image bytes, no request bodies, no auth headers.

Detection conditions (all four must hold):

  1. Previous response on the same session matched the image-processing-error predicate (HTTP 400 + canonical invalid_request_error envelope + image-class message).
  2. Current request carries an image content block whose SHA-256 matches a recorded failure's image hashes.
  3. Current request arrives within the sliding cool-off window.
  4. Current request is on the same session (resolved via x-claude-code-session-id / x-session-id / x-anthropic-session-id).

Sessionless requests bucket to "unknown" — they're not isolated from each other by request signature, an acknowledged limitation mitigated by the 30s sliding window.

Session budget circuit breaker (proxy mode, opt-in)

An opt-in hard per-session spend ceiling. Once a CC session's cumulative token consumption (or its estimated cost, or its consumption rate) crosses a limit you set, further /v1/messages for that session are short-circuited locally — they never reach Anthropic, so they cannot consume credits, trigger auto-purchase, or (for direct API-key users) keep billing the card. Motivated by anthropics/claude-code#68285: a Workflow fan-out of 700+ subagents inherited a premium-tier default with no per-agent model ceiling and no spend gate, burning ~$350 of credits and triggering ~$800 of auto-purchased overage before the user could intervene. All 700 subagents were children of a single session, so a per-session ceiling caps that runaway at the source.

This is a circuit breaker, not a meter. It stops the bleeding; it does not price each request to the cent. The tally is body-sourced (msg.usage token counts, which every Messages response carries) and auth-independent, so it works identically for subscription/OAuth and direct API-key clients.

Opt-in via the gate; default-off, and inert until you set at least one ceiling:

export CACHE_FIX_SESSION_BUDGET=on
export CACHE_FIX_SESSION_BUDGET_COST_USD=25      # e.g. stop this session at ~$25
ModeBehavior
onTally per session; short-circuit the next request once confidently over a ceiling
off (default)Pass-through, no tally, no logging
dry-runTally + log would_block events at the block point, but forward every request (measure before you enforce)

The three blocking levers (set at least one)

Env varDefaultPurpose
CACHE_FIX_SESSION_BUDGEToffGate — on / off / dry-run
CACHE_FIX_SESSION_BUDGET_TOKENSunsetHard-stop when cumulative input + cache_creation tokens for the session cross this integer. Plan-agnostic and exact — the backstop lever.
CACHE_FIX_SESSION_BUDGET_COST_USDunsetHard-stop when estimated cost (tokens × tools/rates.json) crosses this float.
CACHE_FIX_SESSION_BUDGET_RATE_TPMunsetHard-stop when the session's tokens/min over the sliding window cross this integer — the early fan-out catch (fires on the slope, before a big in-flight batch lands).
CACHE_FIX_SESSION_BUDGET_RATE_WINDOW_MS60000Sliding window for the rate lever.
CACHE_FIX_SESSION_BUDGET_MAX_ENTRIES4096LRU cap on the in-memory per-session tally map.
CACHE_FIX_SESSION_BUDGET_EVENT_LOG~/.claude/session-budget-events.jsonlStructured fire-event log path (5 MB single-tier rotation).

Which lever for which billing model

The breaker serves both billing models, but the danger — and so the primary lever — differs:

  • Subscription (OAuth, e.g. Max) — the #68285 case. Tokens are quota-until-overage; the hazard is the account-global auto-purchase wall. Use _TOKENS (or _RATE_TPM) to cap the runaway session before it drives the account into auto-purchase. Cost is informational here.
  • Direct API key (pay-as-you-go) — the more severe case. There is no quota buffer: every token is billed immediately at API list price, and the same fan-out has no spend circuit at all — it charges the card until the key's tier limit or the bank intervenes. Here _COST_USD is a literal dollar ceiling: tools/rates.json is Anthropic's API list pricing, so tokens × rates.json is real money out of pocket.

Cost is an estimate — pair it with a token cap for a guaranteed dollar bound. rates.json may lag a newly-released model. An unknown model contributes 0 to the cost tally (fail-open by design), so a stale rate silently under-counts and can let cost run past your intended dollar figure. The token and rate levers are always exact. If you want a hard dollar cap on an API key, also set _TOKENS so a stale/unknown rate can't let spend run unbounded — the token bound then backstops the estimate. (tools/rates.json is refreshed weekly from Anthropic's pricing page via a fetch-and-open-PR cron; a human reviews every pricing diff.)

Both request modes are covered: streaming responses accrue from the message_start event, non-streaming (stream:false) responses from the returned JSON body. The tally, the ceilings, and the block behaviour are identical either way — there is no mode that bypasses the budget.

Fail-open, always

If accounting is uncertain — gate off, no ceiling set, usage missing or unparseable, no session key, the model unknown to rates.json (cost lever only), first request after a restart, or anything throws — the request forwards. A block requires the gate on and at least one lever numerically, confidently at/over its ceiling. A budget breaker that failed closed would wedge a whole session on a proxy bug, which is worse than the overage. One env flip (CACHE_FIX_SESSION_BUDGET=off) fully disables it.

Observability surface (meter bypass)

A short-circuited request returns before any upstream call, so it produces no usage.jsonl row — correct (no cost was incurred), but note it is not in the meter. The only fire signal is the JSONL event log: each block writes { event: "session_budget_block", would_block, sid, lever, limit, observed, cumulative_tokens, cumulative_cost_usd, request_id, ts }. dry-run writes the same records with would_block: true and forwards. The log carries the tally and the crossed limit only — no request/response bodies, no model-input content, no auth headers. request_id is nullable (a locally-blocked request has no upstream request-id; it's populated from the client request header if present, else null — never fabricated).

Each fire event also carries an observational account_q5h_contribution — an estimate of how much of the account's rolling-5h quota burn this session drove, attributed by the session's token share of the window ({ window_ms, account_q5h_delta, session_token_share, attributed_q5h_delta }). This is derived from Anthropic's account-global anthropic-ratelimit-unified-5h-utilization header, so it is never a blocking lever — it would trip an innocent session for another session's burn. It exists only to show operators which session is driving the account quota. API-key traffic lacks the header, so the field is simply omitted.

The attribution denominator is the proxy's process-global token pool, so the estimate is only sound when a proxy instance serves one Anthropic account (the normal single-operator deployment). If one instance fronts several accounts, independent accounts share the same denominator and a session's contribution can be over- or under-stated. Being observational, this never affects gating.

Known limitations

  • Concurrency overshoot. A large fan-out fires near-simultaneously, so a pure cumulative (_TOKENS/_COST_USD) ceiling trips only after the in-flight batch's tokens land — it overshoots by ~that batch. _RATE_TPM mitigates this by firing on the slope before the batch completes.
  • Output cost is post-hoc — the tally gates the next request, not the current one.
  • Restart resets the tally — it's in-memory; a mid-session proxy restart zeroes it. Acceptable for a safety backstop.
  • Per-session, not per-account — it caps the offending session; it cannot lower Anthropic's account-global quota or stop auto-purchase directly. For a single-session runaway (#68285), capping that session is the right and sufficient action.

cc_version normalize (proxy mode, opt-in)

Some Claude Code distribution channels — notably the VS Code extension under auto-update — emit a cc_version value in the system prompt's x-anthropic-billing-header that includes a per-build hash on top of MAJOR.MINOR.PATCH (e.g. 2.1.185.<buildhash>). When the build hash mutates mid-session (the binary auto-updates between turns), that value lives inside the cacheable prefix, so every subsequent turn pays full cache_creation cost until the suffix stabilizes — Anthropic's prefix cache is byte-exact and the field is in scope.

The existing fingerprint-strip does NOT cover this case: it only rewrites suffixes whose value matches a CC-generated fingerprint of the user message text. A binary build-hash fails that verification and fingerprint-strip returns null without rewriting.

Opt-in via env var; default-off:

export CACHE_FIX_NORMALIZE_CC_VERSION=strip          # collapses X.Y.Z.<suffix> → X.Y.Z
# or
export CACHE_FIX_NORMALIZE_CC_VERSION=pin:2.1.185    # operator-supplied literal
ModeBehavior
off (default)No mutation
stripCollapses cc_version=X.Y.Z(.suffix)+ to cc_version=X.Y.Z
pin:<value>Replaces cc_version=<anything> with the operator literal. Validation: ^[A-Za-z0-9.\-]+$, max 64 chars (anything that would break the surrounding header grammar fails-open to off with a one-shot stderr warning).

The extension runs at order 90, before fingerprint-strip at order 100. After normalization the cc_version has at most 3 segments, so fingerprint-strip's dotParts.length < 4 guard makes it a no-op — the two cooperate cleanly with no other ordering hazards. Field-boundary anchored regex (^|[;\s:])cc_version=([^;\s]+) so a cc_version= substring embedded in another field's value cannot be accidentally rewritten. Atomic fail-open: planned rewrites stage in a local array and apply only after the scan completes; any error during the scan leaves the body byte-intact.

Session backup (proxy mode, opt-in)

A belt-and-suspenders backup against CC's transcript regressions per anthropics/claude-code#66734 (in-place transcript rewrite to a metadata-only stub) and anthropics/claude-code#66486 (missing transcript on interactive sessions). When the proxy is in the path, every assistant message + observed tool result / user input is mirrored into a per-session JSONL file under user control, independent of CC's own transcript writer. CC's transcript remains canonical when it survives; the mirror is the recovery path when it doesn't.

Opt-in via env var; default-off in v4.2.0 and v4.3.0 pending a privacy-posture cycle:

export CACHE_FIX_SESSION_MIRROR=on
Env varDefaultPurpose
CACHE_FIX_SESSION_MIRRORoffMaster gate — on activates mirroring
CACHE_FIX_SESSION_MIRROR_DIR~/.claude/session-mirrors/Storage root
CACHE_FIX_SESSION_MIRROR_MAX_BYTES100 MBPer-session active-file rotation threshold
CACHE_FIX_SESSION_MIRROR_RETENTION_DAYS30Retention sweep horizon (files past this are unlinked)
CACHE_FIX_SESSION_MIRROR_MAX_SESSIONS1024LRU cap on the in-memory dedup state map
CACHE_FIX_SESSION_MIRROR_INCLUDE_THINKINGtrueSet false to exclude thinking content blocks from mirror records

Format-parity: mirror records use CC 2.1.148's verified transcript envelope shape exactly — existing transcript readers (including restore-claude-history-linux) parse mirror files unchanged. The single distinguishing field is source: "cache-fix-proxy-mirror". Three known limitations called out at write time:

  1. cwd is always null (proxy does not know caller working directory).
  2. uuid is dash-formatted (8-4-4-4-12) but the version/variant bits aren't RFC-valid. It's a deterministic hash of (sessionId, timestamp, messageId) so the chain is reconstructable; shape-validating parsers accept it.
  3. Tool-result user records omit toolUseResult and sourceToolAssistantUUID (CC-internal enriched objects the proxy cannot reconstruct).

Storage layout: <DIR>/<sessionFilename(sessionId)>/<timestamp>.jsonl. Session ids that don't match [A-Za-z0-9_-]{1,128} bucket to inv-<sha256[:16]> (path-traversal safe). Sessionless requests share an unknown/ directory.

Operational events (open / rotate / sweep / error) are logged to ~/.claude/session-mirrors/session-mirror-events.jsonl (5 MB single-tier rotation). The mirror is read-only with respect to upstream traffic; no requests or responses are modified, and writer errors are isolated from the response stream by the pipeline's per-hook try/catch.

See docs/disk-usage.md for the worst-case disk-footprint accounting.

Cache breakpoints (proxy mode, opt-in)

Anthropic's prompt cache supports up to four cache_control markers per request. Claude Code currently uses three of the four; the third (between auto-injected messages[0] content — hooks, skills, project CLAUDE.md, deferred tools, MCP server descriptions — and the first real user content) is missing entirely. Without that marker, every change inside the auto-injected span busts the cache for everything that follows. wadabum projected ~6,500 token savings per fresh-session first turn from adding it (anthropics/claude-code#47098).

The proxy can inject the missing marker on opt-in. Default off until validated against community data.

export CACHE_FIX_INJECT_MESSAGES_BREAKPOINT=1

The injection is conservative: it only fires when the request already carries 1–3 markers (typical CC shape) and refuses if the request is at the 4-marker limit (would 400) or has zero markers (Agent SDK / API-direct shape this extension isn't built for). Boundary detection covers all five observed auto-injected block kinds — hooks, skills, CLAUDE.md, deferred-tools, MCP — and lands the marker on the LAST auto-injected block.

A diagnostic-only env var dumps the structural shape of messages[0] for fixture sourcing without mutating the request:

export CACHE_FIX_DUMP_MESSAGES_HEAD=/tmp/messages-head.jsonl
Env varDefaultPurpose
CACHE_FIX_INJECT_MESSAGES_BREAKPOINTunsetEnable breakpoint #3 injection (=1 opt-in).
CACHE_FIX_DUMP_MESSAGES_HEADunsetDiagnostic JSONL dump of messages[0].content shape — read-only, no mutation.

Microcompact stability (proxy mode, opt-in)

After ~90 minutes idle, Claude Code's time_based_microcompact (and the cold-compact path triggered by FDY()) replaces old tool_result content with a sentinel string. The original content is gone for cache purposes; that part is unrecoverable from the proxy. But the sentinel itself can carry an embedded timestamp ([Old tool result content cleared at 2026-04-30T13:42:11Z]), which means a second microcompact pass against the same already-cleared position writes different bytes — busting the cache for everything after that position even though no new content was added.

This extension addresses the recoverable half: normalize the sentinel to a byte-stable canonical form so repeat microcompacts don't churn the cache. Phase 1 only — diagnostic + opt-in normalization. Phase 2 (snapshot-and-restore of original tool_result content) is deferred to v3.5.0+ pending Phase 1 production data.

# Step 1 (diagnostic): characterize what CC's sentinel actually looks like.
export CACHE_FIX_DUMP_MICROCOMPACT=/tmp/microcompact-dump.jsonl

# Step 2 (normalize): once the sentinel format is confirmed, opt-in.
export CACHE_FIX_NORMALIZE_MICROCOMPACT=1

Detection has two modes:

  • Mode A — exact match against confirmed CC sentinel patterns (the bare form and the ISO-8601 timestamp variant). Mode A matches are eligible for normalization.
  • Mode B — prefix-only match (text begins with [Old tool result content cleared but does not exactly match a Mode A pattern). Mode B is diagnostic-only: never normalized, dump records redact to a 64-char prefix only.

The Mode A/B separation protects against cases where the sentinel might be followed by user-derived content (e.g., a tool that echoed user input back into its result) — the redaction guarantee on Mode B keeps that content out of the diagnostic dump.

Env varDefaultPurpose
CACHE_FIX_DUMP_MICROCOMPACTunsetPath for diagnostic JSONL dump of detected sentinels. Read-only — no mutation.
CACHE_FIX_NORMALIZE_MICROCOMPACTunsetEnable normalization (=1 opts in). Mutates Mode A matches to canonical form.
CACHE_FIX_MICROCOMPACT_NORMALIZED[Old tool result content cleared]Override the canonical replacement string.
CACHE_FIX_MICROCOMPACT_SENTINEL_PATTERN_<N>unsetAdd custom Mode A regex pattern(s). Numbered (1-indexed, sparse OK).
CACHE_FIX_MICROCOMPACT_SENTINEL_PREFIX_<N>unsetCustom Mode B literal prefix(es). Pair with a custom Mode A pattern from a non-default sentinel family so prefix-only variants of that family also get redacted Mode B capture.
CACHE_FIX_MICROCOMPACT_REDACT_LEN64Mode B prefix length in dump records. Set to 0 to suppress the prefix entirely.
CACHE_FIX_DUMP_MICROCOMPACT_INCLUDE_NORMALIZEDunsetAdd post-normalization text alongside (not replacing) raw sentinel_text in dump records.

Thinking summaries (proxy mode, opt-in, Opus 4.7+)

On Opus 4.7, Anthropic flipped the API default for thinking.display from "summarized" to "omitted". In parallel, Claude Code's CLI has a !getIsNonInteractiveSession() gate that propagates display: "summarized" only when the session is interactive. The combination means every CC subprocess spawned with --input-format stream-json — the VS Code chat panel, the Antigravity panel, the SDK, claude --print — sends a thinking-enabled request (thinking.type is either "enabled" or "adaptive" depending on CC version) without display, and the API responds with thinking blocks whose thinking field is empty (plus a multi-KB signature). The UI shows a static "Thinking" stub while the agent runs but never any reasoning content.

Upstream root cause and patch proposed in anthropics/claude-code#59844 (credit: @ojura). This extension is the proxy-side complement: when a request to an Opus 4.7 endpoint has thinking enabled but display unset, inject the configured mode at the API boundary. Works on any CC version routed through cache-fix-proxy, no waiting on Anthropic to ship the CLI fix.

# Restore summaries (the built-in default — non-interactive surfaces get reasoning content)
export CACHE_FIX_THINKING_DISPLAY=summarized

# Force-suppress override (agent runtimes that don't want thinking blocks at all)
export CACHE_FIX_THINKING_DISPLAY=omitted

# Explicit no-op (extension passes through unchanged)
export CACHE_FIX_THINKING_DISPLAY=disabled

The extension is default-on as of v3.6.1. The cache-prefix test measured 0% absolute drop in steady-state cache_read ratio when injection is active on Opus 4.7 (5 sequential claude -p calls per window, baseline vs injected — both windows held 1.000 cache_read ratio from call 2 onward). Adding thinking.display to the request body changes the bytes Anthropic hashes, but Anthropic's cache layer accepts and indexes the injected-prefix the same way it does any other prefix. Users who want the older "no injection" behavior (e.g. to avoid any request-body mutation at all) explicitly set CACHE_FIX_THINKING_DISPLAY=disabled.

Scoping rules baked into the extension:

  • Model-gated. Only fires on requests whose model matches /^claude-opus-4-7/ — covers claude-opus-4-7 and claude-opus-4-7-1m. Sonnet 4.7 needs separate verification (the API default-flip may differ); future versions (4.8+) require an explicit cache-fix bump rather than auto-applying unverified behavior.
  • User opt-out preserved. If the request already

常见问题

What is claude-code-cache-fix?

claude-code-cache-fix is an open-source cli tools skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by cnighswonger. Fixes prompt cache regression in Claude Code that causes up to 20x cost increase on resumed sessions. It has 418 GitHub stars.

Is claude-code-cache-fix safe to use?

Yes. claude-code-cache-fix passed SkillsLLM's automated security scan — a dependency vulnerability audit plus prompt-injection heuristics — with no high-severity issues. You can read the full report in the Security Report section on this page.

How do I install claude-code-cache-fix?

Clone the repository with "git clone https://github.com/cnighswonger/claude-code-cache-fix" and add it to your Claude Code skills directory (see the Installation section above).

What programming language is claude-code-cache-fix written in?

claude-code-cache-fix is primarily written in JavaScript. It is open-source under cnighswonger on GitHub, so you can review or fork the full source.

Are there alternatives to claude-code-cache-fix?

Yes. SkillsLLM lists many other CLI Tools skills you can browse and compare side by side. Open the CLI Tools category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh claude-code-cache-fix against similar tools.

评论 (0)

暂无评论,成为第一个分享想法的人!

ui-ux-pro-max-skill

by nextlevelbuilder

12

An AI skill that provides design intelligence for building professional UI/UX across multiple platforms.

119,92012,870Python
CLI 工具ai-skillsantigravity
查看详情

happy

by slopus

Mobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured

23,4501,980TypeScript
CLI 工具
查看详情

claudecodeui

by siteboon

Use Claude Code, OpenCode, Cursor CLI, and Codex on mobile and web with CloudCLI (aka Claude Code UI). CloudCLI is a free open source webui/GUI that helps you manage your Claude Code session and projects remotely.

13,3941,866TypeScript
CLI 工具
查看详情

CRS-自建Claude Code镜像,一站式开源中转服务,让 Claude、OpenAI、Gemini、Droid 订阅统一接入,支持拼车共享,更高效分摊成本,原生工具无缝使用。

12,5471,869JavaScript
CLI 工具
查看详情

ccstatusline

by sirmalloc

🚀 Beautiful highly customizable statusline for Claude Code CLI with powerline support, themes, and more.

12,508545TypeScript
CLI 工具
查看详情

开发者还喜欢

基于喜欢此 Skill 的开发者投票和收藏

ECC

by affaan-m

10

The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

242,21936,702JavaScript
AI 智能体ai-agentsanthropicclaude-code
查看详情
15

An agentic skills framework & software development methodology that works.

234,96620,863Shell
AI 智能体ai-agentsbrainstorming
查看详情

hermes-agent

by NousResearch

10

The agent that grows with you

234,43747,175Python
AI 智能体ai-agentsagent-orchestration
查看详情

n8n

by n8n-io

12

Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.

201,88160,308TypeScript
MCP 服务器apisai-tools
查看详情

The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

185,94028,768JavaScript
AI 智能体ai-agentsanthropicclaude-code
查看详情

cc-switch

by farion1231

3

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

128,8688,826Rust
AI 智能体claude-codeai-tools
查看详情