Metadata-Version: 2.4
Name: fatcheck
Version: 0.2.0
Summary: Weigh-in for CLAUDE.md and AGENTS.md
Author-email: shunvel <shunvel@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/shunvel/fatcheck
Project-URL: Repository, https://github.com/shunvel/fatcheck
Keywords: agents,claude,codex,cursor,AGENTS.md,CLAUDE.md
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13
Requires-Dist: pyyaml>=6
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

# fatcheck

Agent instruction files (`CLAUDE.md`, `AGENTS.md`, and the rest) only grow. Adding a rule is cheap. Deleting one without knowing *why* it was there feels unsafe. fatcheck is a weigh-in: count the rules, record the why in a sidecar the agent never loads, and prune only after you pick the cuts.

```bash
uvx --from git+https://github.com/shunvel/fatcheck fatcheck scan
```

## Demo (messy-repo)

Before, on the bundled fixture:

```
              fatcheck scan
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━┓
┃ file                           ┃ kind   ┃ instr ┃ lines ┃ bytes ┃ ~tok ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━┩
│ .cursor/rules/ts.mdc           │ cursor │     1 │     7 │   121 │   30 │
│ .github/copilot-instructions.md│ copilot│     1 │     1 │    47 │   11 │
│ AGENTS.md                      │ codex  │     4 │     6 │   168 │   42 │
│ CLAUDE.md                      │ claude │     2 │    15 │   171 │   42 │
│ frontend/AGENTS.md             │ codex  │     1 │     1 │    38 │    9 │
│ shim/CLAUDE.md [shim]          │ claude │     0 │     1 │    11 │    2 │
└────────────────────────────────┴────────┴───────┴───────┴───────┴──────┘
total instructions: 9
~136 tokens (bytes/4, estimate)
```

`lint` flags uncommented rules and a TypeScript line copied into both `CLAUDE.md` and `AGENTS.md`. After notes plus `prune --apply`, the extra copy and the falsified on-call rule are gone:

```diff
--- AGENTS.md
+++ AGENTS.md
@@ -1,6 +1,5 @@
 # Agents
 
 - Use TypeScript for all new application code.
 - Run tests before every pull request.
 - Prefer small pull requests.
-- Always ping the on-call before merging.

--- CLAUDE.md
+++ CLAUDE.md
@@ -1,5 +1,4 @@
 # Team
 
-Use TypeScript for all new application code.
 Never commit API keys or secrets.
```

TypeScript stays in `AGENTS.md` (default canonical). Notes live in `.fatcheck.yml`, not in the prompt.

## Install

Python 3.11+. Until the package is on PyPI:

```bash
uvx --from git+https://github.com/shunvel/fatcheck fatcheck scan
# or
pipx install git+https://github.com/shunvel/fatcheck
```

From a clone:

```bash
pip install -e ".[dev]"
```

## Day-one flow

First `lint` is a wall of `missing`. Freeze that, then CI only fails on **new** uncommented rules.

```bash
fatcheck scan
fatcheck init --baseline
fatcheck lint              # new missing / copies (size is a scan warning)
fatcheck lint --strict     # all missing, ignore baseline
fatcheck lint --off missing
fatcheck annotate          # numbered why (writes .fatcheck.yml only)
fatcheck prune             # numbered diffs, nothing written
fatcheck prune --apply     # pick 1,3 or all, then y/N (writes *.bak)
fatcheck prune --apply --only all --yes   # scripts; --yes alone will not apply everything
```

`--json` and `--path` work on scan, lint, and prune. `lint --sarif fatcheck.sarif` writes SARIF 2.1 for GitHub code scanning. Exit 0 when clean, 1 when there are findings. `lint` never writes `.fatcheck.yml` (hash rematch is applied by `init` / `annotate`).

## Hooks (write-time, fail open)

Copy-paste, not a plugin per vendor. If an agent instruction file changed, remind the maintainer to record why.

| Agent | Drop-in |
| --- | --- |
| Cursor | [examples/cursor/hooks.json](examples/cursor/hooks.json) + [after-agent-file.sh](examples/cursor/after-agent-file.sh) / [after-agent-file.py](examples/cursor/after-agent-file.py) → `.cursor/hooks.json` and `.cursor/hooks/` |
| Claude Code | [examples/claude/settings.hooks.json](examples/claude/settings.hooks.json) + [post-tool-use.py](examples/claude/post-tool-use.py) → `.claude/settings.json` hooks + `.claude/hooks/` |
| Codex / Copilot / Gemini | [examples/AGENTS.fragment.md](examples/AGENTS.fragment.md) |

Other repos can pin this project as a pre-commit hook:

```yaml
- repo: https://github.com/shunvel/fatcheck
  rev: v0.2.0
  hooks:
    - id: fatcheck       # copies / noise; does not fail on uncommented rules
    - id: fatcheck-new   # new missing vs .fatcheck-baseline.yml (run init --baseline first)
```

Optional `.fatcheck.toml` in the repo root:

```toml
canonical = "agents"    # agents | claude | longest
ignore_local = true     # skip CLAUDE.local.md
```

## How to read the output

- **~tok** — `bytes/4`, a rough prompt-size estimate, not a tokenizer.
- **missing** — no note in `.fatcheck.yml` (grandfathered per file if listed in `.fatcheck-baseline.yml`).
- **noise** — a note that is too vague.
- **copied_file** — two whole files look like copies. Keep the canonical one (`agents` by default).
- **duplicate** — the same rule in two files that are otherwise different.
- **size_claude** / **size_codex** — over the published soft limits (200 lines / 32 KB). Shown by `scan`, not a `lint` failure.

`prune` proposes **prune_falsified** (your note says the rule did not help; if `recurrence >= 1`, it says “failed N times”) and **prune_duplicate**. Safety-marked rules are left alone. Deletes match the current instruction text, so a moved line still gets found.

## Files it finds

No config required. Among others: `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, `.cursor/rules/`, `GEMINI.md`. It skips `README.md`, `.git`, `node_modules`, `vendor`, `site-packages`, `.venv`, `generated`, `third_party`, and paths deeper than 8 directories. `CLAUDE.local.md` is skipped unless `ignore_local = false`.

## GitHub listing (set on the repo)

Description: `Weigh-in for CLAUDE.md and AGENTS.md`. Topics: `claude-code`, `agents-md`, `cursor`, `prompt-engineering`.

Consumer SARIF workflow (copy into *your* repo): [examples/github/sarif.yml](examples/github/sarif.yml). Publishing to PyPI is tag `v*` via [.github/workflows/publish.yml](.github/workflows/publish.yml) (trusted publishing; not uploaded from a local session).

## Where this idea comes from

Kushal Chakrabarti, [Why Does CLAUDE.md Keep Growing? Catastrophic Remembering in Agentic Coding](https://arxiv.org/abs/2608.11095) (arXiv:2608.11095). Public GitHub agent files more than tripled over their life; net adds piled up; old rules almost never came out except in a wipe. The paper’s advice is to write what failed, what you tried, and whether it helped. fatcheck is an independent tool. It is not affiliated with the paper’s author.

Today’s coding tools put the whole instruction file into the prompt. fatcheck keeps notes in `.fatcheck.yml` so the agent never sees them.

## Honest limits

fatcheck cannot recover why a two-year-old rule exists. It does not invent a why. It only deletes lines after `prune --apply` and a pick you confirm. Leave prune off CI.

## Tests

```bash
pytest
ruff check src tests
```

Checklist: [tests/TEST_MATRIX.md](tests/TEST_MATRIX.md). Fixture: [fixtures/messy-repo](fixtures/messy-repo).
