Linter + profiler + coverage, but for agent context.
ctxlint audits, profiles, and scores the context files steering AI coding agents —
AGENTS.md, CLAUDE.md, .cursor/rules/*.mdc, .github/copilot-instructions.md, and
skills. It answers the questions nobody can currently answer about their repo:
- What does my agent actually load? Per tool, per directory, in what order, and why.
- What does it cost? Token totals for the context injected on every request.
- Which rules are broken? Duplicated across tools, drifted apart, contradictory, pointing at files that no longer exist, or buried so deep they get lost.
- Do they help? Judge recent commits against your rules to find violations and dead rules that never apply to anything.
The npm package is @tqakdev/ctxlint; the command it installs is ctxlint.
npx @tqakdev/ctxlint scan # one-off, no install
npm i -g @tqakdev/ctxlint # then run `ctxlint` anywherenpx @tqakdev/ctxlint scanThat's it — no config needed. You get a Context Health Score (0–100), a per-tool table of exactly which files each agent loads and what they cost, and a list of findings you can act on in under a minute each.
Running ctxlint scan on a repo with hand-maintained, drifted context files:
ctxlint — 5 context file(s), 95 rules
Context Health Score: 42/100
freshness 0 uniqueness 0 consistency 70 budget 80 structure 82
Context files
file kind tools tokens≈ rules
.cursor/rules/broken.mdc cursor-rule cursor ≈37 2
.cursor/rules/style.mdc cursor-rule cursor ≈184 7
.cursorrules other — ≈56 5
.github/copilot-instructions.md copilot-instructions copilot ≈210 9
CLAUDE.md claude-md claude-code ≈2812 72
claude-code @ . — ≈2812 tokens always-on
# file tokens≈ why
1 CLAUDE.md ≈2812 CLAUDE.md at repo root
cursor @ . — ≈184 tokens always-on (+ ≈37 conditional)
# file tokens≈ why
1 .cursor/rules/broken.mdc ≈37 frontmatter unparseable — activation unknown, assumed not auto-attached
2 .cursor/rules/style.mdc ≈184 alwaysApply: true
Findings: 15 error(s), 6 warning(s), 2 info
✖ [duplication] Same rule maintained twice for different tools: .cursor/rules/style.mdc:10-11
and CLAUDE.md:65-66 are 100% identical. Keep one canonical copy (prefer AGENTS.md) and
delete the other.
✖ [stale-reference] CLAUDE.md:45-49 references `src/api/v1/` which does not exist —
actively misleading the agent. Update the reference or delete the rule.
▲ [drift] These rules started identical and diverged (66% similar):
.github/copilot-instructions.md:7-8 vs CLAUDE.md:104-105.
diff: … against the [-main-] {+develop+} branch and …
▲ [contradiction] Contradictory instructions about the same thing (polarity heuristic
is English-only):
A: "Always use named exports in shared modules."
B: "Never use named exports for React components."
▲ [budget] 9 critical rule(s) buried past 70% depth of an oversized file (CLAUDE.md,
deepest at line 217 = 99%) — likely lost in long sessions. Move critical rules to the front.
ℹ [load-semantics] .cursorrules is read by no tool ctxlint knows (legacy format) —
migrate its rules into .cursor/rules/*.mdc or AGENTS.md, then delete this file.
Discovers every context surface (respecting .gitignore, skipping symlinks and files
over 1 MB), splits them into atomic rules, resolves per-tool load semantics, runs five
analyzers, and prints the report.
| flag | what it does |
|---|---|
--format text|json|md |
output format (default text) |
--output <file> |
write the report to a file |
--ci |
exit 1 when error-severity findings exist |
--max-files <n> |
cap the discovery walk on huge monorepos |
Writes ctxlint-fixes.md with every fix grouped into safe (auto-applicable) and
suggestions (need your judgment). --write applies only the safe set:
- delete exact duplicates, keeping the canonical copy (AGENTS.md ranks highest);
- update stale paths when git history shows a unique rename target;
- move buried critical rules to the front — only ones the author CAPITALIZED (NEVER/MUST/ALWAYS); lowercase judgment calls stay suggestions.
--write refuses to run on a dirty git tree, so every change is reviewable and
revertable.
Samples your last N merged changes (default 30; falls back to plain commits), skips
lockfiles/vendored/generated code, prefilters (rule, diff-chunk) pairs by file and
keyword overlap, then asks a model for a strict-JSON verdict per pair:
followed | violated | not-applicable with a one-line evidence quote.
- Spend cap: total cost is estimated up front; anything above $1 (configurable)
requires
--yes. - Disk cache: verdicts are keyed by (rule, chunk, model) so reruns are incremental and nearly free.
- Dead rules: rules that apply to nothing across the sample are called out — they cost tokens on every request and never change behavior.
--calibrate: re-judges a 10% sample with a second model and prints the agreement rate. Below 80%, the report tells you — prominently — to treat per-rule scores as directional only.
Regenerates the report from .ctxlint-cache/last-scan.json without re-scanning
(--format, --output as above).
Add a Context Health check that comments on every PR:
# .github/workflows/ctxlint.yml
name: context-health
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
ctxlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: tqakdev/ctxlint/action@main
with:
fail-on-error: "true" # gate the job on error-severity findings
fail-on-score: "0" # optionally require a minimum Context Health Score (0 disables)
comment: "true" # post/update the report as a PR commentThe action also writes the report to the job summary and exposes score and errors
as outputs.
Everything has a default aimed at typical repos; create ctxlint.config.json only to
change something (schema in ctxlint.config.schema.json):
{
"budgets": { "surfaceWarnTokens": 1500, "effectiveContextWarnTokens": 4000 },
"compliance": { "model": "claude-haiku-4-5", "commits": 30, "spendCapUsd": 1 }
}| section | option | default | meaning |
|---|---|---|---|
| budgets | surfaceWarnTokens |
1500 | warn when one file exceeds this many estimated tokens |
| budgets | effectiveContextWarnTokens |
4000 | warn when one tool's always-on context exceeds this |
| budgets | buriedRuleDepthRatio |
0.7 | flag critical rules deeper than this fraction of an oversized file |
| discovery | maxFiles |
20000 | hard cap on the repo walk (--max-files overrides) |
| analysis | maxRules |
5000 | pairwise analyzers bail gracefully above this |
| compliance | model / calibrationModel |
haiku / sonnet | judge and second-opinion models |
| compliance | spendCapUsd |
1 | require --yes above this estimated spend |
Honesty section. Read this before trusting a number.
- Token counts are estimates. Offline counting uses js-tiktoken (
o200k_base) and is labeled≈everywhere; vendor tokenizers differ, so treat counts as ±10–20%, more for CJK-heavy content. WithANTHROPIC_API_KEYset,scanfetches exact Anthropic counts and labels them exact. - The Context Health Score is deterministic, not divine. Same input, same score —
the formula is documented in
src/core/scoring.ts(five weighted subscores, fixed penalties per finding). It's a trend instrument: watch it move in CI, don't worship the absolute number. - Compliance verdicts are judge-based. An LLM reads a rule and a diff and gives an
opinion. Run
--calibrateto measure cross-model agreement; below 80% the report itself tells you the scores are directional. Dead-rule detection depends on your sample size — a rule that applied to nothing in 30 commits might apply next week. - Load semantics are best-effort. Tool behavior is encoded from documented
behavior; where it's undocumented, the table says "(assumed)" instead of presenting
a guess as fact. Legacy
.cursorrulesis treated as loaded by nothing (assumed). - Some heuristics are English-only. Duplication/drift shingles work on any language; polarity-based contradiction detection only understands English always/never phrasing, and says so in each finding.
- Pairwise analysis is O(n²). Above 5000 rules (configurable), duplication/drift analysis bails gracefully with a note rather than hanging your CI.
pnpm install
pnpm test # vitest, fixture-driven — no network calls anywhere in tests
pnpm typecheck # TS strict
pnpm lint # biome
pnpm build # emits dist/, runnable as node dist/cli.jsThe three fixture repos under test/fixtures/ are the test bed: clean-repo scores
100, messy-repo triggers every finding category, monorepo exercises subtree and
glob-scoped resolution.
MIT