Skip to content

Releases: gethrbr/context-tax

0.5.0

Choose a tag to compare

@shoustak shoustak released this 12 Oct 13:18
9a943c7

Added

  • Claude Code's own tools are a lever. A permissions.deny rule 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, fix denies 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.
  • fix asks 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, max the ones under
    1 in 5. --level picks one without asking; a run that cannot ask gets safe.
  • A plugin used rarely is offered at the lean or max level. 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 fix did. Its first lines give the exact first-request median
    before and after it, once a session has started since. fix logs 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 as plugin:<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

Choose a tag to compare

@shoustak shoustak released this 10 Oct 14:39
a277dd2

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 /, so context-tax session in ~/my_app or 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.json puts its servers at the top level is read. Claude Code loads
    that layout from a plugin, with or without the mcpServers wrapper, 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.json still 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

Choose a tag to compare

@shoustak shoustak released this 22 Sep 13:25
92b20fc

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 url is not a URL no longer ends every report with a stack trace. A ${API_BASE} the environment did not fill, an empty url, or a host with no scheme made the run throw TypeError: 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 --svg and fix --yes into a path that cannot be written are one sentence each, not a stack trace. fix writes 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. --top and --window are whole numbers, --cwd cannot be empty, a sub-second --timeout says within 250ms.
  • Every measuring command says what it started and which hosts it contacted, on one line under the spinner. The receipt's PAID block is SO FAR.

Documentation

  • The fix screens in the README are generated from a fixture and pinned by a test, like the other screens. "fix is 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 in dist/ 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

Choose a tag to compare

@shoustak shoustak released this 21 Sep 19:47
5e7a1f3

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 ITSELF is itemised: the client's own tools and system prompt are rows, so unattributed is 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 -p and 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

Choose a tag to compare

@shoustak shoustak released this 10 Sep 10:20
4d83146

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

Choose a tag to compare

@shoustak shoustak released this 07 Sep 05:15
d26c7cc

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.0 established 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.json committed a year earlier, 0.2.1 reported 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 user servers and plugins the machine enabled are judged machine-wide, while a .mcp.json server, a ~/.claude.json entry 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 start finding 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

Choose a tag to compare

@shoustak shoustak released this 04 Sep 15:12
8df71be

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 servers

No 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-tax

v0.2.0

Choose a tag to compare

@shoustak shoustak released this 04 Sep 08:09
4cb3cb9

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/4 caveat 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 /clear and /compact you 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, > file and | less are 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-tax

v0.1.1

Choose a tag to compare

@shoustak shoustak released this 03 Sep 12:02
287a83e

The empty run.

npx context-tax

context-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 null token count still prints the table. null means 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.
  • PROBLEMS is 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

Choose a tag to compare

@shoustak shoustak released this 02 Sep 14:21
9d72c3e

First public release.

npx context-tax

Every 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
    usage your API calls were billed on. The rows are estimates, and the difference is printed as
    its own unattributed row 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 --yes is 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, measure and evidence for the three passes on their own, plus a library surface
    so each is usable without the terminal rendering.
  • A deferred column. 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.