Repository navigation
Releases: gethrbr/context-tax
Release list
0.5.0
Added
- Claude Code's own tools are a lever. A
permissions.denyrule naming a tool takes it out of
what is sent, and the report used to call the whole block "nothing here to switch off". Each tool
is now priced from the session's own record and counted in sessions: never used,fixdenies it;
used now and then and worth at least 500 tokens, it is denied at the level you pick, and every
line says what stops working. Measured before it was written, in an interactive session: one tool,
11,402 tokens of a 47,700-token first request. - Agents that are never called are denied with
Agent(<name>). The agent listing has no cap,
unlike the skill listing, so an agent taken out is that many tokens gone. The client's own agents
are never offered, and an idle plugin's agents leave with the plugin. fixasks how much to switch off. In a terminal it shows three levels, each with the tokens
it recovers and what it adds by name, and asks:safe(the default) is what you do not use,lean
adds Claude Code's own tools and the plugins you use in under 1 session in 20,maxthe ones under
1 in 5.--levelpicks one without asking; a run that cannot ask getssafe.- A plugin used rarely is offered at the
leanormaxlevel. One call in 300 sessions kept a 164-skill
plugin off the plan for good. It is now priced as everything it sends, and the line names the
hooks that stop with it, because a hook is use no transcript records. - The report says what the last
fixdid. Its first lines give the exact first-request median
before and after it, once a session has started since.fixlogs what it writes to
~/.cache/context-tax/fixes.jsonl; a fix made before the log is found from its backup. --level <safe|lean|max>.
Fixed
- 🚨 A plugin's MCP server is kept apart from a server you declared with the same name. The
client loads both and lists the plugin's asplugin:<plugin>:<name>. They were folded into one row,
which gave the plugin the other server's calls and kept it off the plan as "in use".
0.4.2
Fixed
-
🚨 A directory whose path holds a dot, an underscore, a space or any other non-alphanumeric
character has its sessions found. Claude Code files a directory's sessions under its path with
every character that is not an ASCII letter or digit turned into-, and cuts a name past 200
characters with a hash of the full path appended. This replaced only/, socontext-tax sessionin~/my_appor in a worktree under.claude/said there was no session history,
and the auto-memory file of such a project was never found. The rule is now the client's own,
checked against folders Claude Code 2.1.292 created, the hashed case included. -
🚨 A plugin whose
.mcp.jsonputs its servers at the top level is read. Claude Code loads
that layout from a plugin, with or without themcpServerswrapper, and a probe plugin shipped
that way was connected by 2.1.292. This reported such a file as "not loaded" under PROBLEMS and
left its servers out. A project.mcp.jsonstill needs the wrapper, as the client's schema
requires, and is still reported without one. -
🔒 A path inside a PROBLEMS sentence starts with
~, like the path printed under it. The
sentence spelled out the home directory, username included.
0.4.1
An audit of the published package, run against fabricated machines that look nothing like the one it was built on: no ~/.claude, malformed transcripts, an 18-server MCP zoo, a 155 MB transcript, Windows paths, byte-order marks, read-only directories, Node 20. Nothing here changes what the tool is for; everything here is a way it could have been wrong, rude, or loud on somebody else's machine.
Fixed
- 🚨 An MCP server whose
urlis not a URL no longer ends every report with a stack trace. A${API_BASE}the environment did not fill, an emptyurl, or a host with no scheme made the run throwTypeError: Invalid URL. It is now a row that names the placeholder that was not set, never the URL itself.${VAR}and${VAR:-default}in a server definition are filled in the way Claude Code fills them. - 🚨 A small skill listing with one bare name no longer proves a window. It used to derive "a window of about 12,000" and print "292% of a window" on the first line and on the receipt.
- 🚨 The first-request number is the first billed request. The old rule skipped every session whose first call was a cache hit and could take a mid-session cache miss as the opening size, at two to five times the truth. The screen says first request where it said cold start.
- 🔒 A URL is printed as its scheme and host. Hosted MCP gateways put the secret in the path. A failing launcher's echo is scrubbed of its arguments, a malformed settings file is reported by position rather than quoted, a failed remote probe never echoes the URL, and the MCP handshake sends the real version instead of
0.1.0. session --svgandfix --yesinto a path that cannot be written are one sentence each, not a stack trace.fixwrites the new text first, then the backup, then the rename, so nothing is copied for a file it cannot write; what was written before a failure is reported as written.- The receipt says so when its rows sum past the total, and exits 1 like the main screen.
- A settings file that starts with a byte-order mark is read.
--topand--windoware whole numbers,--cwdcannot be empty, a sub-second--timeoutsayswithin 250ms. - Every measuring command says what it started and which hosts it contacted, on one line under the spinner. The receipt's
PAIDblock isSO FAR.
Documentation
- The
fixscreens in the README are generated from a fixture and pinned by a test, like the other screens. "fixis the only part that writes" was false and now reads "the only command that writes to your settings". The library example typechecks. Two links that 404 on npmjs.com are absolute. Comments that ship indist/no longer name the machine the tool was built on.
405 tests, 30 guards mutation-checked, every 0.4.0 crash scenario re-run against the auditor's fabricated homes. Full list in the changelog.
v0.4.0
The rows are read, not modelled. A recent Claude Code writes what it sent into the session transcript: the skill listing as it went out, the tool names, each instruction file, its own tools and system prompt. context-tax now reads that record, and keeps the model for a machine that has none. A line under the table says which one the rows came from.
What your agent never received
Claude Code gives the skill listing a budget of 1% of the context window. Past it, every skill keeps its name and descriptions compete for what is left, so the ones that lose reach the model as a bare name. Nothing on screen says so.
It is now the first finding when it happens: how many, grouped by who owns them, and both ways out priced. Raise skillListingBudgetFraction and pay for every description, or switch skills off and hand their room on. context-tax fix --restore-descriptions writes the first. It is never part of a plain fix, because it is the one change here that makes every turn cost more.
Two screens made to be shared
context-tax receipt: the same totals by kind, with no server, plugin, skill or path on it.context-tax session [id] [--svg file]: one session turn by turn. The climb, each compaction, and the floor it never drops under, which is the fixed prefix the main screen itemises.
Also new
- The first line says the share of your window, and only when a session proves the window. The window is worked back from a listing pinned against its budget, never assumed.
CLAUDE CODE ITSELFis itemised: the client's own tools and system prompt are rows, sounattributedis the gap between characters and billed tokens and not most of the screen.- Connectors no file declares, such as one attached to a claude.ai account, are rows, read from what was sent.
Fixed, and these change your numbers
- 🚨 One reply is counted once. Claude Code writes a single API reply as several transcript lines, and every line repeats the reply's
usage. Earlier versions counted lines, so turns, tokens carried and every per-call figure ran at about twice the truth. Those figures drop on upgrade, and the new ones are right. - 🚨 A plugin's MCP server is no longer reported as never called. Its tools are keyed
mcp__plugin_<plugin>_<server>__, and the join looked for the bare name. - A server that could not connect is charged nothing and gets no finding.
- A server's per-turn cost is what a deferring client sends: its tool names and instructions, not every tool description.
claude -pand SDK runs no longer stand in for a session a person started.
Privacy
The transcript record holds the full text of your instruction files and skill descriptions. The parser takes lengths, counts and names from it and drops the text on the same line, so none of it reaches the report, --json or the library API. A test asserts that.
Verified before tagging
377 tests across 15 files on Node 20, 22 and 24. 27 guards mutation-checked: the bug put back, a test seen to fail. Packed locally and grepped rather than reasoned about: 93 files, the same top level as 0.3.1, and no name, path or count from a real machine anywhere in the tarball.
Full changelog: v0.3.1...v0.4.0
v0.3.1
No behaviour change. A patch cut for one reason: 0.3.0 is on the registry carrying two figures measured off a real machine.
tsc keeps comments, so they reached dist/ and therefore the published tarball, in three files. Merging the fix did not change what anyone installs, because what npx context-tax installs is the tarball. Only a release does.
A tool whose whole argument is that nothing leaves your machine should not ship a count taken off one, however harmless the integer. The replacement reproduces the same bug from a corpus that is nobody's history: twelve sessions, every one belonging to another directory.
Also in this release
vitest 3.2.7 to 5.0.0, a development dependency that is not in the tarball. Same 267 tests across the same 12 files, on Node 20, 22 and 24.
Verified before tagging
Packed locally and grepped rather than reasoned about: same 79 files and the same top level as 0.3.0, and 0 files in the packed dist/ carry the figures, against 3 in the published 0.3.0.
Full changelog: v0.3.0...v0.3.1
v0.3.0
Nine defects, found by auditing the published 0.2.1 against configurations nobody here designed: a directory with no history, a machine two sessions old, a transcript that cannot be opened, a server that cannot start, and twelve servers at once with four of them pathological.
Every one is the same shape, which is why they are one release: the tool said something it could not defend, in a place where saying nothing was available.
Three that would have been visible on somebody else's machine
- 🚨 A verdict no longer counts sessions the thing could never have been loaded in.
0.2.0established that a denominator must cover everything its fix would switch off. Nothing established the ceiling, so a project with too little history of its own borrowed the machine's for everything, including servers that exist in one directory. In a directory that had never run Claude Code, with a.mcp.jsoncommitted a year earlier,0.2.1reported the server as never called across every session on the machine, about a server that had been in context for none of them, and printed a fix that would have disabled it. Widening is now earned by reach:-s userservers and plugins the machine enabled are judged machine-wide, while a.mcp.jsonserver, a~/.claude.jsonentry filed under one project, and a plugin this repo enabled are judged here or not at all. - 🚨 A server that failed to start is no longer priced from the bundled fallback table, which had been dropping both the
cannot startfinding and the spawn error behind it.--no-spawn, which never asked, still uses the table. - 🚨 One unreadable transcript no longer ends the run with a Node stack trace and exit 1. The scan finishes and names what it could not read, because the sessions inside those files are missing from every denominator on the screen.
And six more
N skills never invoked now needs the same five sessions a server has always needed. A screen where nothing could be judged no longer reads as an all-clear. Agents invoked as Task, before the tool was renamed Agent, are counted. The calls column takes a dash where there is no history to count. Two verdicts stop counting to zero out loud. --fix stops telling you to restore a backup of a file that never existed.
Upgrading
Minor rather than patch: Ledger gained judged and Evidence gained unreadable, both required on types exported from api.ts. The CLI is unaffected.
Full detail in CHANGELOG.md and #19.
v0.2.1
A flag is not consent
Starting a project MCP server runs whatever that repo's .mcp.json names. The rule has always been that standing in the directory is consent, because your own session starts the same command every turn — and a --cwd flag is not.
The check asked a looser question: is either path inside the other. So a target sitting underneath the working directory read as the monorepo case:
cd ~/projects && context-tax --cwd ./just-cloned # ran the clone's serversNo session in ~/projects ever loads that file. Found by pointing 0.2.0 at seven freshly cloned repositories — one declared uvx arxiv-mcp-server, and the run tried to start it thirty seconds after git clone. Three HTTP servers from another clone were handshaked for the same reason.
Trust is now anchored on the project root the servers were resolved for. Both real monorepo directions keep working — pointing up at the root from a package, or down at a package from the root — and a nested checkout, which is its own project and never yours, is refused.
The rule moved into trust.ts because it could not be tested where it was: index.ts dispatches at module scope, so importing it runs the CLI. That is why no test could have caught this, and there are nine now.
A fix named a file you could not open
The line telling you which settings file to edit was clamped at three lines, like the diagnostic prose above it. On a deep path the … ate the path — the actionable half of the one actionable line. Two characters shorter and it survived.
The path is now printed whole, $HOME collapses to ~ so the usual one fits on a single line, and a path too long even for that starts on its own line. It still hard wraps rather than overhanging: every screen fits the window it was given is asserted in a test.
Also
1 memory files. A row that counts to one now says so.
Not breaking
Patch, not minor. api.ts is untouched, no exported type changed, and the new module is internal.
Full notes in CHANGELOG.md.
npx context-taxv0.2.0
The recommendation that would have broken your other repo
context-tax judged every server on the sessions in the directory you ran it from. For a server whose only lever is claude mcp remove <name> -s user, that is the wrong denominator: the command is machine-wide. Found on a real machine — one call in this project, 67 in a sibling checkout, and the removal printed against it anyway.
A verdict's denominator now has to cover everything its fix would switch off. Machine-wide levers need machine-wide silence. Project-scoped levers keep the project's count.
The first run in an unfamiliar directory
In a fresh clone, or anywhere you have not used Claude Code before, share, calls and per call all came out empty and every verdict downgraded to "too few sessions to judge". Those are the three columns that carry the whole argument, blank in the run you are most likely to make first — while the history to fill them sat on the same disk.
A project with too little history now borrows the machine's denominator and says on this machine wherever it did.
The exact total does not widen. A cold-start median mixed across projects with different configs is not this directory's prefix, and it will not be labelled as one.
The scan was narrow because a whole-corpus pass was assumed to cost a minute. Measured: 890 transcripts and 2.1 GB in 5.1s, inside a command that already spends ten seconds starting servers.
The screen
- Leads with the number. It used to open with four lines of
chars/4caveat and keep its one exact figure at the bottom of the table. The methodology now sits below, next to the total it qualifies. Nothing was removed. - A line saying what this machine has actually spent: sessions, turns, tokens of context carried, and the
/clearand/compactyou typed to survive it. - A progress line. A run takes ten seconds and every one of them was silent, which reads as a hang. It names the server it is waiting on. Written to stderr and only when stderr is a terminal, so
--json,> fileand| lessare byte-for-byte what they were. - Rows sharing a note collapse onto one line, and a row measured from the bundled table says so rather than printing the same bare dash as a row that could not be measured at all.
Breaking, for library users
Verdict carries a required scope, LedgerRow a basis, Ledger a machine, and MeasureOptions an optional onProbe. The CLI is unaffected.
Full notes in CHANGELOG.md.
npx context-taxv0.1.1
The empty run.
npx context-taxcontext-tax in a directory with no MCP servers, skills, agents or memory files, and no session
history to join them against, drew the whole table with 0 or - in every cell. That is the run
somebody arriving from a link is most likely to make first, and an empty grid reads as a tool that
failed rather than as a machine with nothing on it.
It now says what is true, and what to do about it:
context-tax ~/somewhere
NOTHING TO MEASURE HERE
No MCP servers, skills, agents or memory files were found for this
directory, and no session history on this machine to join them against. That
is nothing to bill rather than a bill of nothing.
This reads what your agent loads here and what your own sessions actually
called, so it has something to say from a repository where you use Claude
Code. Point it at one with --cwd <path>, or cd there and run it again.
The column legends and the NOT MEASURABLE HERE caveat leave with the table, because both of them
explain a screen that is no longer being printed, and a caveat about the limits of measuring nothing
is indistinguishable from a bug.
What it deliberately does not do
- A
nulltoken count still prints the table.nullmeans an item could not be measured, which is
a real cost of unknown size rather than an absence, and that run has something to show. - A machine with sessions on record but nothing loaded also keeps the full screen. It still has a
billed total to reconcile against. PROBLEMSis reported on both paths. A file that could not be read is news whether or not
anything else was found.
Full changelog: v0.1.0...v0.1.1
v0.1.0
First public release.
npx context-taxEvery article about MCP context bloat ends with the same advice: audit which tools your agent
actually calls, and prune the rest. None of them automate that sentence. This does.
It reads what your config loads, measures what that weighs over a real MCP handshake, reads your own
session transcripts for what you actually called, and multiplies.
What it does
- The report. What every line item costs per turn, joined against what your sessions actually
called, with a verdict and a routed fix per row. The per-turn total is exact, taken from the
usageyour API calls were billed on. The rows are estimates, and the difference is printed as
its ownunattributedrow rather than smeared across the numbers above it. context-tax fix, the only command that writes. Plans without touching disk, shows the literal
diff of the bytes it will land, backs up what it replaces to~/.cache/context-tax/backups/, and
refuses to write from a non-interactive stdin unless--yesis passed. It never touches
~/.claude.json, which sits beside your API keys: a backup of that file would be a second copy of
every credential on your machine.config,measureandevidencefor the three passes on their own, plus a library surface
so each is usable without the terminal rendering.- A
deferredcolumn. Current Claude Code loads MCP tool schemas on demand rather than carrying
them in every prompt, so a server has two costs paid at different times. Collapsing them into one
number would misreport one of them.
What it refuses to say
No claim of dead weight without its denominator. Server age comes from git, never from file mtimes,
because git checkout rewrites mtimes and an mtime-based tool goes quiet exactly when it should
speak. A server that could not start is unmeasured, never 0.
Notes
- Zero runtime dependencies. Not "few". None, asserted against the import graph by the test suite
and again in CI. - No telemetry, no model call, no endpoint. Every verdict is arithmetic.
- Token counts are
chars / 4, checked by execution to within 4%. - No dollar figure is printed, because a wrong one is more convincing than a wrong token count.
- Requires Node 20 or newer.
- 193 tests, every guard mutation-checked: the bug is put back and the test has to fail.
Published from CI with npm provenance, so the tarball is attested to this tag and this workflow run.