uvx --from git+https://github.com/dorkian/contextlint contextlint
No API key. No network. No telemetry. No dependencies.
Install · Install via AI assistant · Usage · Scheduled health checks · Dashboard · Reproducing the numbers · How it compares · PrivacyThe problem in one chart
Every skill, instruction file and MCP tool schema your assistant loads is injected into the system prompt before your first message. You pay for it on every request, including the ones where none of it is relevant.
Existing tools price a skill by its size on disk. But a well-formed skill's body only loads when it is invoked — what you pay every turn is its one-line catalog entry. Conflate the two and you are wrong by an order of magnitude, in the direction that flatters the savings claim.
contextlint separates the two, prices them independently, and refuses to add together numbers that mean different things.
What it looks like
--html report.html writes a self-contained visual report: a treemap of always-on cost coloured by whether you have ever invoked the asset, a cost-against-usage scatter, and filterable findings. One file, no CDN, works offline, respects your system theme.
Live Dashboard
A local React + Vite dashboard over the same data, for when running it or screen-sharing it beats reading a terminal.
The overview tells the whole story before any scrolling: the always-on total against your context window, a diagram of why it splits the way it does — drawn live from the current report, not illustrated once — and the safe-to-reclaim / worth-reviewing split, kept apart the same way the CLI keeps them apart. Below that: always-on cost by asset as a ranked, square-root-scaled column chart; cost against real usage; findings you can filter, search and click through to a file path; and an Apply Fixes flow that mirrors the CLI's own git-dirty-tree guard rather than silently bypassing it with --allow-dirty.
cd dashboard
npm install
npm run dev
Open http://localhost:3535. Point it at any project by clicking the path in the header, or load ?path=/absolute/path/to/project directly — bookmarkable, or embeddable behind a fixed link. The server binds to 127.0.0.1 only and never builds a shell command from a string (execFile with an argument array throughout) — the exact class of exposure this tool exists to flag, closed in the one place here that could have had it.
Install
Not on PyPI yet — install from the repository. Python 3.10+, zero required dependencies.
# run it once, install nothing
uvx --from git+https://github.com/dorkian/contextlint contextlint
# or put `contextlint` on your PATH
uv tool install git+https://github.com/dorkian/contextlint
pipx install git+https://github.com/dorkian/contextlint
pip install git+https://github.com/dorkian/contextlint
Two optional extras, neither needed for the default path:
uv tool install "dorkian-context-lint[exact] @ git+https://github.com/dorkian/contextlint" # tiktoken, measured counts
uv tool install "dorkian-context-lint[api] @ git+https://github.com/dorkian/contextlint" # anthropic, for --tokenizer anthropic
The distribution name is dorkian-context-lint — PyPI's namespace-similarity check rejects the shorter contextlint. The installed command is unaffected: it's still contextlint, matching everything above.
Updating, and working on it locally
A git install pins the commit it was built from, so upgrade explicitly:
uv tool upgrade contextlint # or: uv tool install --force git+https://...
To hack on it:
git clone https://github.com/dorkian/contextlint && cd contextlint
pip install -e ".[dev]"
python -m pytest tests -q
Cutting a release is a maintainer task, documented in RELEASING.md — it publishes to PyPI over Trusted Publishing (OIDC) on a version tag, no token stored anywhere.
Install via your AI assistant
Zero required dependencies and a single git URL means there is nothing for an assistant to get wrong. Paste this into Claude Code, Cursor, Copilot, Codex CLI, or anything else with terminal access:
Install the contextlint CLI from https://github.com/dorkian/contextlint. It is not
published to PyPI yet, so do not run a bare `pip install contextlint` — it will 404.
Install directly from git instead: use `uv tool install
git+https://github.com/dorkian/contextlint` if uv is available, otherwise `pipx install
git+https://github.com/dorkian/contextlint`, otherwise `pip install --user
git+https://github.com/dorkian/contextlint`. Then run `contextlint --version` to confirm
it worked, and `contextlint audit --no-global` in this project to show me a real report.
Also using Claude Code? Set it up as a skill too
skill/contextlint/SKILL.md in this repo wraps the CLI so Claude can run and interpret an audit on request, without you typing a single flag. Add this to the prompt above, or run it on its own:
Also install this as a Claude Code skill: run `mkdir -p ~/.claude/skills/contextlint &&
curl -fsSL
https://raw.githubusercontent.com/dorkian/contextlint/main/skill/contextlint/SKILL.md -o
~/.claude/skills/contextlint/SKILL.md`. Then, in future sessions, I can just ask about my
context budget, dead skills, or MCP security and you'll know to reach for contextlint.
Three things nothing else does
1. It reads your session history
Static analysis tells you a skill is large. Only the transcripts tell you that you have never once used it. Those two facts lead to opposite actions.
Nothing leaves your machine — only tool-invocation names and timestamps are parsed, never prompt or response text.
2. It treats MCP security as a first-class check
Unauthenticated remote servers · plaintext HTTP · credentials in env, in headers, and in argv · unpinned npx -y supply chains · shell-launched servers · filesystem servers rooted at / · blanket permission rules · and instruction-shaped text hidden in tool descriptions, including zero-width and Unicode-tag characters that render as nothing to you and as text to the model.
It also measures what a server actually costs, which configuration alone cannot tell you:
3. It separates what is certain from what is a judgement call
Deleting a byte-identical duplicate cannot change behaviour. Deleting a skill you have not invoked in 90 days might.
Usage
contextlint # audit this project + your global config
contextlint --html report.html # visual report
contextlint --format json # machine-readable
contextlint --assistant cursor # one assistant only
contextlint --no-global # this project only
contextlint --tokenizer tiktoken # exact counts
contextlint --fail-on high # exit 1 in CI when something serious is found
contextlint --mcp-probe # measure real MCP tool schemas (starts your servers)
contextlint checks # list adapters and checks
contextlint fix # print a plan; changes nothing
contextlint fix --apply # apply it, with a backup and an undo
contextlint restore <backup-dir> # undo a fix run, on any platform
contextlint watch --every 6h # keep checking, and report what changed
contextlint watch --once --quiet # one check with drift — the form for cron
Keeping it healthy over time
Agent config rots the way dependencies rot: nobody adds 4,000 tokens on purpose, it arrives 200 at a time. watch re-runs the audit and reports drift rather than the same numbers again.
$ contextlint watch --every 6h
2026-09-07T08:42:31+00:00 · OK · always-on 18,703 (9.4%) · +412 since last · new 1 · findings 96
+ MCP server '#' publishes # tools costing # tokens every turn
Each run appends a small snapshot to .contextlint-history.jsonl — aggregates and finding fingerprints, never asset content. Fingerprints strip numbers out of titles, so a server going from 26 to 27 tools is not reported as one finding resolved and another appearing.
| Flag | Default | Fails the check when |
|---|---|---|
--warn-at PCT |
5% |
always-on reaches this share of the window (warn only, exit 0) |
--fail-at PCT |
15% |
always-on reaches this share of the window |
--max-growth N |
off | always-on grew by more than N tokens since the previous check |
| — | always on | any open critical finding, or a new high-severity finding |
--once is the form to put in cron, systemd, launchd or a scheduled CI job: something else owns the scheduling, and contextlint still knows what the numbers were yesterday. --quiet prints nothing while healthy, so cron only mails you when it is not. Exit code is 0 for ok and warn, 1 for fail.
Ready-made snippets for cron, launchd, systemd, GitHub Actions and pre-commit are in docs/scheduling.md. This repository runs the check on itself in .github/workflows/context-health.yml.
Why drift is sometimes suppressed instead of reported
Switching --mcp-probe on, or changing --tokenizer, changes what is being measured. Comparing a probed run against an unprobed one would report a multi-thousand-token swing that never happened, so watch says measurement mode changed and reports no drift for that tick rather than inventing one.
Why MCP tool schemas are unmeasured by default
Configuration says a server exists. It cannot say the server publishes 26 tools whose schemas cost 3,517 tokens on every turn — that number only exists on the wire. So by default contextlint counts servers, reports their schema cost as unmeasured, and excludes it from the totals rather than inventing a per-tool average.
--mcp-probe measures it properly: it runs the MCP handshake against each server and prices the real tools/list response. It starts your servers to do that, so it prints exactly what it is about to run and asks first.
Why fix is deliberately timid
It only removes what is provably safe: byte-identical duplicates and empty assets. Everything else needs your judgement and stays in the report.
- It prints a plan and changes nothing without
--apply. - It refuses to run on a dirty git tree, so
git checkout .is always a valid undo. - It copies every removed file into
.contextlint-backups/<timestamp>/with a manifest and a generatedrestore.sh, so the undo works outside git too.contextlint restore <backup-dir>replays it on any platform, and refuses to overwrite a path that exists again unless you pass--force.
What it parses, per assistant
| Assistant | Sources |
|---|---|
| Claude Code | .claude/skills/*/SKILL.md, .agents/skills/, plugin skills, .claude/agents/, .claude/commands/, the CLAUDE.md chain, auto-memory files, .mcp.json, .claude.json (global and per-project servers), settings.json permissions and hooks |
| Codex / AGENTS.md | AGENTS.md at root and nested, ~/.codex/config.toml MCP servers |
| Cursor | .cursor/rules/*.mdc (respecting alwaysApply vs globs), .cursorrules, .cursor/mcp.json |
| GitHub Copilot | .github/copilot-instructions.md, .github/instructions/*.instructions.md (respecting applyTo), .github/prompts/ |
Loading mode is tracked per asset — always, on_demand, conditional, deferred — because that, not file size, is what determines cost.
The checks
| id | What it looks for |
|---|---|
budget |
Total always-on cost, share of the context window, the assets driving it |
mcp-cost |
Real tool-schema cost per MCP server (with --mcp-probe) |
duplication |
Byte-identical copies, same-name shadowing, near-duplicates |
hygiene |
Missing frontmatter or description, oversized descriptions, empty and bloated assets |
usage |
Installed but never invoked; stale; MCP servers never called |
security |
Unauthenticated and plaintext-HTTP servers, credentials in config or argv, unpinned supply chains, shell launches, filesystem scope creep, tool-poisoning and hidden-character patterns, blanket permission rules |
Commands
| Command | Writes anything? | What it is for |
|---|---|---|
contextlint audit |
no | the full report — terminal, JSON or HTML |
contextlint watch |
history file only | the same audit on a schedule, reporting drift |
contextlint fix |
no | print the plan for the safely-removable subset |
contextlint fix --apply |
yes | apply it, behind a clean-git-tree guard, with a backup |
contextlint restore |
yes | replay a backup, refusing to clobber |
contextlint checks |
no | list the adapters and checks in this version |
Reproducing the numbers
Every figure in this README has a command attached. That is the point.
python benchmarks/run.py # detection + savings
uv run --no-project --with tiktoken python benchmarks/calibrate.py # tokenizer error
uv run --no-project --with tiktoken python benchmarks/tune.py # refit the heuristic
| Measurement | Result | How |
|---|---|---|
| Planted defects detected | 15 / 15 | benchmarks/run.py against benchmarks/fixtures/bloated |
| Savings from automatic fixes | 14.0% of always-on tokens | before/after audit of a fixture copy, real fix path |
| Tests | 109, on Linux/macOS/Windows × Python 3.10–3.13 | CI |
The 14% is small on purpose. It is what an automatic, behaviour-preserving fix actually recovers. The larger number — the assets you have never invoked — is reported separately and left to you, because deleting them is a decision, not an optimisation.
About the tokenizer
The default counter is an offline approximation: a GPT-style pre-tokenizer plus per-character-class length rules, with seven parameters fitted against tiktoken. It ships with the parameters fitted on half the corpus, so the published error is genuinely out-of-sample: +2.82% aggregate, 3.90% median on the held-out half.
It is calibrated on agent configuration — markdown, JSON schemas, YAML frontmatter — and its error on other kinds of text is unmeasured. For exact counts use --tokenizer tiktoken, or --tokenizer anthropic for a specific Claude model via the free count_tokens endpoint (opt-in; it sends your config text to the API, which is why it is not the default).
How it compares
Verified against the GitHub API, not taken from anyone's README.
| contextlint | Claude token optimizers 4 projects, up to 113★ |
MCP security scanners 8+ projects, up to 44★ |
|
|---|---|---|---|
| Token accounting | ✅ | ✅ | — |
| Prices by loading mode, not file size | ✅ | — | — |
| Correlates against real session usage | ✅ | — | — |
| MCP config security checks | ✅ | — | ✅ |
| Measures live MCP tool-schema cost | ✅ | — | — |
| Cross-assistant | ✅ 4 | — | n/a |
| Scheduled health check with drift | ✅ | — | — |
| Visual report | ✅ | — | — |
| Reproducible benchmark harness | ✅ | — | — |
| Separates certain from candidate savings | ✅ | — | — |
The two niches are each crowded; the intersection was empty. gh search repos "context window audit agent skills" returns zero results.
Privacy
The default path reads local files, makes no network connection, and writes nothing. Asset content never appears in the JSON or HTML output — only names, paths and counts.
Two modes are exceptions, both opt-in and both announced at the point of use: --mcp-probe starts your configured servers, and --tokenizer anthropic sends configuration text to the Anthropic API. See SECURITY.md.
Design notes
- One adapter per assistant, one file per check. Adding an assistant is one file and one registry entry.
- No required dependencies. A tool whose thesis is that dependencies have a cost should demonstrate that it believes it.
- Findings are signals, not verdicts. Every security finding names the pattern it matched and why, so you can dismiss it in ten seconds when it is wrong.
- Never claim a percentage the harness cannot reproduce.
- Charts are generated, not drawn.
docs/assets/make_charts.pybuilds every graphic above from measured JSON, and CI fails if a committed chart no longer matches its data.
Full contract in SPEC.md. The dogfood audit is written up in docs/case-study.md.
Related work
Four Claude Code token optimizers exist — valorisa/Claude-Skills, Sharan0516/claude-token-optimizer, 4pixeltechBR/token_saver_ClaudeCode, KINGSTAR-OMEGA/claude-token-optimizer — alongside a separate cluster of MCP security scanners. contextlint sits in the gap between them. Sharan0516's router pattern — replacing always-loaded descriptions with a lightweight catalog — is the strongest idea in the existing field and directly informed how loading modes are modelled here.
Licence
MIT
Metadata
Release files for dorkian-context-lint 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dorkian_context_lint-0.1.1.tar.gz | 765.1 kB | With provenance | Details for dorkian_context_lint-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dorkian_context_lint-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for dorkian_context_lint-0.1.1-py3-none-any.whl |
Total release size: 835.6 kB
Release files / dorkian_context_lint-0.1.1.tar.gz
| Download URL | dorkian_context_lint-0.1.1.tar.gz |
|---|---|
| Size | 765.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
15f334e246f84d5064a38366757e4fab75dc8d4eeffdd0135f77ff2c50952135
|
|
BLAKE2b-256 checksum How to use checksums |
283fc40aa4eaae18b8d00fd56af50391bdcf5d77a2e7e4341165ac55f9d6570e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logRelease files / dorkian_context_lint-0.1.1-py3-none-any.whl
| Download URL | dorkian_context_lint-0.1.1-py3-none-any.whl |
|---|---|
| Size | 70.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f2065255faff9f9f646d1e8a2c56b4ded535d3bf458acb8c70dbf665c48d574a
|
|
BLAKE2b-256 checksum How to use checksums |
38de5707a3a0bdbbe62e6b7f6aa43ea6b69bd2b9df4502bf52dd55a089eb3108
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logSecurity for dorkian-context-lint 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for dorkian_context_lint-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.