unrot
v0.6.0
Published
Lint AI agent config files (CLAUDE.md, AGENTS.md, .cursorrules, skills, MCP configs) for staleness, bloat, contradictions, and broken references.
Maintainers
Readme
unrot
Your agent configs rot. The repo moves on — files get renamed, scripts get replaced, conventions change — while CLAUDE.md, AGENTS.md, .cursorrules, and the rest quietly keep describing a codebase that no longer exists. Every session your agents start from instructions that are stale, bloated past what models actually follow, contradictory, or pointing at files that are gone. Nothing checks them.
unrot finds the rot: a linter for the config files that steer your AI coding agents — CLAUDE.md, AGENTS.md, .cursorrules, Claude Code skills and commands, MCP configs, Copilot instructions. Pure static analysis + git: no network calls, no telemetry, no LLM calls, and it never modifies your files.

Quick start
npx unrot checkZero config required. Run it at the root of any repo.
# Inventory every agent config file in the repo
npx unrot scan
# JSON output + non-zero exit on errors — works as a CI gate
npx unrot check --jsonOr install it: npm i -D unrot, then run unrot (or agent-config-linter — both binaries are installed).
Not affiliated with the separate
agentlintnpm package.
Sample output
A real run on cline/cline:
$ npx unrot check
.claude/commands/hotfix-release.md
⚠ 187 lines (warn threshold: 100) — long instruction files get partially ignored (oversized)
→ Tighten wording and move rarely-needed detail into referenced docs
sdk/AGENTS.md
⚠ 109 lines (warn threshold: 100) — long instruction files get partially ignored (oversized)
→ Tighten wording and move rarely-needed detail into referenced docs
✖ Referenced path "./DOC.md" does not exist in the repo:9 (broken-refs)
→ Fix the path or delete the stale reference
1 error, 2 warningsFound in the wild
From a validation run across 69 popular open-source repos (Next.js, VS Code, React, Django, prisma, storybook, supabase, zed, ollama, cline, codex, OpenHands, ...), with every error-level finding individually verified against the repo:
- microsoft/vscode — the Copilot extension's
AGENTS.mdreferences four source files that no longer exist; for one, unrot spots the file's new location: Did you mean "...common/skillConfigLocations.ts"? - openai/codex —
AGENTS.md:35tells agents to usecodex-rs/codex-mcp/src/mcp_connection_manager.rs, which isn't in the repo. - sst/opencode — a committed
AGENTS.mdtells agents to verify against/Users/kit/code/..., a path that exists on exactly one maintainer's laptop. - langchain-ai/langchainjs — ships an
AGENTS.mdover 400 lines long, well past where models reliably follow every rule. - BerriAI/litellm —
@-imports an 11.8KBCLAUDE.mdinto every session, from two separate files. - 49 of the 69 repos had agent configs at all; 36 of those 49 had findings.
These were shallow clones, so the staleness rule — which reads git history — never ran. (A --depth 50 clone reaches about 50 commits of a linear history, though merge-heavy repos retain far more, and staleness does fire on those.) A full clone would likely surface more, not less.
Commands
unrot scan [path] [--json] [--no-color]
unrot check [path] [--json] [--no-color] [--config <file>] [--rules <a,b>]
unrot fleet <target> [--json] [--no-color] [--config <file>] [--concurrency <n>] [--keep] [--token <t>]| Exit code | Meaning |
|---|---|
| 0 | No error-severity findings |
| 1 | check/fleet found at least one error |
| 2 | Usage or runtime failure |
What it finds
scan discovers, at any depth (monorepos included, .gitignore respected):
CLAUDE.md / CLAUDE.local.md · AGENTS.md · GEMINI.md · .cursorrules · .cursor/rules/** · .clinerules (file or folder) · .windsurfrules · .rules (zed) · .goosehints · .agents/skills/**/SKILL.md · .claude/skills/**/SKILL.md · .claude/settings.json · .claude/commands/**/*.md · .mcp.json · .github/copilot-instructions.md · .github/instructions/*.instructions.md
check runs these rules:
| Rule | Default severity | What it catches |
|---|---|---|
| staleness | warn | Config last touched >90 days ago while the repo gained >100 commits — it likely describes an older codebase. Uses git history. |
| missing-config | warn | An active repo (≥20 commits, ≥5 source files) with no agent config at all. |
| oversized | warn / error | Instruction files past the size where models start dropping rules: warn >100 lines or >10KB, error >200 lines. |
| contradictions | warn / info | Files that disagree on package manager or indentation (warn); commit conventions defined in multiple non-identical files (info). |
| broken-refs | error | @-imports, markdown links, backtick paths, and npm/pnpm/bun/yarn run scripts that don't exist anywhere in the repo. When the missing file exists elsewhere (renamed extension, moved directory), the finding says so: Did you mean "..."? |
| wrong-level | warn | Personal content in committed files — /Users/<name>/... paths, "I prefer...", "my machine" — which belongs in user-level ~/.claude/CLAUDE.md. |
| eager-embeds | warn | @-imports that inline a large file (>10KB) into every session; suggests a conditional pointer instead. |
The rules are deliberately conservative. broken-refs, for example, forgives paths that resolve deeper in a monorepo, build artifacts (dist/... or anything matching a .gitignore rule), gitignored-but-present files, setup-time files (.env*), placeholder paths (./foo.ts, src/xxx/xxx.feature), package specifiers, script-family mentions (npm run watch:*), references hedged with "if/unless ... exists", and @-imports shown inside code spans or fences (which Claude Code doesn't evaluate either) — every reported error should be worth fixing.
Configuration (optional)
Create .unrot.json at the repo root to tune thresholds, change severities, or disable rules (.agentlint.json also works; .unrot.json wins if both exist):
{
"rules": {
"oversized": { "warnLines": 150, "errorLines": 300 },
"staleness": { "maxAgeDays": 60, "minCommitsSince": 50 },
"eager-embeds": { "maxEmbedBytes": 20480 },
"missing-config": { "severity": "off" },
"broken-refs": { "severity": "warn" }
}
}- Every rule takes
"enabled": falseor"severity": "off"to disable it. "severity": "error" | "warn" | "info"overrides the severity of everything a rule reports (errors drive the exit code).--rules staleness,oversizedruns only the listed rules, ignoring enabled/disabled state.--config path/to/file.jsonpoints at an alternative config file.
JSON output
--json prints a stable schema for CI:
{
"schemaVersion": 1,
"root": "/path/to/repo",
"files": [
{ "path": "CLAUDE.md", "kind": "claude-md", "size": 2048, "modified": "2026-05-01T12:00:00.000Z" }
],
"findings": [
{
"rule": "broken-refs",
"severity": "error",
"file": "CLAUDE.md",
"line": 12,
"message": "Referenced path \"docs/setup.md\" does not exist in the repo",
"suggestion": "Fix the path or delete the stale reference"
}
],
"summary": { "errors": 1, "warnings": 0, "infos": 0 }
}file is null for repo-level findings (e.g. missing-config).
Symlinked configs (e.g. CLAUDE.md -> AGENTS.md, a common way to share one source of truth across agent tools) are linted once: the entry for the real file carries an "aliases" array with the other paths, and terminal output notes them as (also linked as CLAUDE.md).
CI example (GitHub Actions)
Copy-paste job — fails the build only on error-severity findings (exit 1):
lint-agent-configs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history lets the staleness rule run
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Lint agent configs
run: npx unrot check --jsonTune or silence rules for CI via .unrot.json (see Configuration above).
Fleet scanning
Scan many repos at once and get one combined health report:
unrot fleet gh:your-org # every repo of a GitHub org or user (skips archived + forks)
unrot fleet repos.txt # a file listing repos, one per line (owner/repo or full git URL)
unrot fleet ./work/ # a local directory whose subdirectories are repos
unrot fleet gh:your-org --json # schemaVersion-2 JSON for dashboards/CIFleet report for gh:your-org
Repo Configs Errors Warnings Health
your-org/api 4 2 3 C
your-org/webapp 2 0 1 B
your-org/cli 1 0 0 A
your-org/data-jobs 0 0 0 —
4 repos scanned, 3 have agent configs, 2 have findings
2 errors, 4 warnings
Worst offenders:
your-org/api (2 errors)
CLAUDE.md: Referenced path "docs/setup.md" does not exist in the repoHealth grades:
- A — configs present, no findings.
- B — findings, but nothing error-severity.
- C — 1–2 errors.
- D — 3 or more errors.
- — — no agent configs at all.
Remote repos are shallow-cloned (--depth 50) into a temp dir that is deleted afterwards (--keep to retain). The staleness rule works best with full history, so shallow fleet scans may under-report staleness. Repos that fail to clone or scan are listed at the end of the report and never abort the run. GitHub listing works unauthenticated for public repos; pass --token (or set GITHUB_TOKEN) for private repos and higher rate limits.
Fleet scanning is strictly read-only: it never opens PRs or modifies the scanned repos. Keeping configs in sync across repos is Phase 3 on the roadmap — tell us how your team would use it.
Scope
unrot is read-only static analysis — single-repo (scan/check) plus multi-repo fleet reporting. No auto-fix, no config sync (yet — see the roadmap note above), no LLM calls.
Accuracy is measured two ways, both against real repos:
- Precision — on a 20-repo held-out corpus spanning Java, PHP, C#, Ruby, Go, C++, and Swift (ecosystems the rules were never tuned on), every error-level finding was individually verified against the repo: 15 of 16 held up on the first run, and the one false positive it surfaced has been fixed. Earlier releases were validated the same way across 69 open-source repos.
- Recall — a mutation harness injects known rot into real config files and checks that the matching rule fires: 354 of 356 injected defects caught (99.4%). Alongside those, 440 of 440 negative controls stayed quiet, confirming the forgiveness heuristics don't fire on valid references, hedged phrasing, or commented-out prose.
License
MIT
