Your repository can be perfectly valid and still be full of code nobody needs anymore. AI coding agents have to search through that repository, understand it, test around it, and sometimes get distracted by it.
Diet Code finds structurally unreachable code — deterministically, with no LLM — and lets you measure whether removing it makes agents do less work.
diet-code analyze .
↓
dead-code evidence (FACTS, not scores)
↓
diet-code clean # deterministic, CERTAIN/HIGH only
↓
minimal Git diff on diet-code/<timestamp>
↓
tests/build pass (fail-closed: revert on failure)
↓
diet-code benchmark # same agent task on BASE vs DIET
↓
BEFORE/AFTER agent-work table
Diet Code is not an "AI-powered dead code detector." Traditional tools ask: "Is this code unused?" Diet Code asks: "After removing this obsolete code, does an AI agent actually do less work?" The benchmark is the wedge; the detector is infrastructure.
npm install -g @mohammadpalla/diet-code
diet-code install # registers the skill with your assistantThen, in Claude Code (or Cursor, Codex, Amp, opencode — see --agent):
/diet-code is calculateLegacyTax needed
Verdict: NOT NEEDED (CERTAIN)
Evidence
- 0 references across 1,284 indexed symbols
- not exported; not reachable from any entry point
- no dynamic usage detected near it
History
- created 2022-04-18, last touched 2024-01-09 by Alice
- "migrate auth to OAuth (#1821)" — the commit that removed its last caller
Recommendation
- safe to remove: `diet-code clean` covers it
The split matters: the analyzer proves whether anything reaches the code
(deterministic, no model), and the agent reads Git history to explain why
it is still there. A commit saying "temporary fallback, keep until v2 rollout"
overrides a CERTAIN — and the skill instructs the agent to say so.
The skill never deletes anything. Removal only happens through
diet-code clean --apply, after you approve the printed plan.
The skill is a single file, skills/diet-code/SKILL.md,
compiled into the binary so install works from every distribution channel.
It is: a local-first static repository analyzer + deterministic cleanup tool + agent benchmark for TypeScript/JavaScript and Python.
It is not: an AI refactoring assistant, MCP server, context server,
code-indexing SaaS, or generic linter. There is no LLM inside the
analyzer — cargo run works with no API key, no model, no network.
You do not need Rust. Diet Code ships as a self-contained binary through npm (and a shell installer); the Rust toolchain is only for building from source.
# npm (recommended — Node 18+, no Rust, no native build)
npm install -g @mohammadpalla/diet-code
npx @mohammadpalla/diet-code analyze . # or run without installing# shell installer (Linux/macOS, installs to ~/.local/bin, verifies checksum)
curl -fsSL https://raw.githubusercontent.com/Mohammad-Palla/diet-code/master/scripts/install.sh | sh# from a checkout (Rust stable) — not published to crates.io
git clone https://github.com/Mohammad-Palla/diet-code && cd diet-code
cargo install --path crates/diet-code-cliThe npm package downloads the prebuilt binary for your platform
(linux/macOS/Windows × x64/arm64) and verifies its SHA-256 checksum; the
launcher is a ~50-line zero-dependency shim that execs the Rust binary.
Overrides: DIET_CODE_SKIP_DOWNLOAD=1, DIET_CODE_DOWNLOAD_BASE_URL=…,
DIET_CODE_BINARY_PATH=/path/to/diet-code.
# Analyze the current repository (human summary + .diet-code/analysis.json)
diet-code analyze .
diet-code analyze . --json # stable JSON schema (v1)
diet-code analyze . --verbose # all findings, not just CERTAIN/HIGH
# Explain one symbol or file with its deterministic evidence + Git history.
# Works whether the code is dead (finding + confidence) or alive
# (KEPT, with what reaches it).
diet-code explain src/utils/oldAuth.ts
diet-code explain calculateLegacyTax
diet-code explain --path /path/to/repo <symbol-or-file>
# Register the `/diet-code` skill with your AI coding assistant
diet-code install # Claude Code, this project
diet-code install --global # Claude Code, all your projects
diet-code install --agent cursor # .cursor/rules/diet-code.mdc
diet-code install --agent agents # AGENTS.md (Codex, Amp, opencode, ...)
diet-code install --dry-run # show the target and body, write nothing
# Preview the deterministic patch plan (changes nothing)
diet-code clean --dry-run
# Apply it on a new diet-code/<timestamp> branch (CERTAIN/HIGH only),
# re-run the analyzer, run verification scripts, fail closed (revert)
# unless everything passes. --keep retains the branch on failure.
diet-code clean
diet-code clean --keep
# Benchmark: same agent tasks on BASE vs DIET, fresh dir + fresh session
# per run, randomized order, medians reported.
diet-code benchmark --tasks benchmarks/got/tasks.json --reps 3
diet-code benchmark --tasks tasks.json --reps 1 --agent-model opencode/muse-spark-1.3-contributor-free
diet-code benchmark --tasks tasks.json --mock # plumbing check, no agenttasks.json format:
[
{
"name": "add-rate-limit",
"prompt": "Add rate limiting to the payment API. Keep changes minimal.",
"verify": "NODE_OPTIONS='--import=tsx/esm' ./node_modules/.bin/ava test/rate-limit.ts",
"testFile": "rate-limit.test.ts"
}
]verifydecides success — never the agent's prose. It runs in each fresh workdir after the agent finishes.testFile(optional) is copied from besidetasks.jsoninto every run workdir astest/diet-task-<name>.ts, identically in both arms.--agent-commandoverrides discovery (opencodepreferred,claudefallback,--mockfor CI).--agent-modelselects the model (defaultopencode/muse-spark-1.3-contributor-free).- Reports persist to
.diet-code/benchmark-<timestamp>.json.
An agent solving "add response.redirected" in a 300-file repo must list,
read, and rule out files: locales it will never touch, example apps for
other frameworks, benchmark harnesses, vendored scripts. Every irrelevant
file is tokens in, tool calls out, and another chance to get distracted
(e.g. editing the docs example instead of the source).
Diet Code's hypothesis: removing genuinely unreachable structure reduces measurable agent work (tokens, tool calls, files read) without changing task success. v0.1 exists to test that claim, not to assert it.
- Traversal — recursive walk; ignores
.git/ node_modules/ dist/ build/ coverage/ .next/ .nuxt/ .turbo/ .cache/ vendor/ target/plusdiet-code.jsoninclude/exclude and.dietignore; no symlinked dirs; parallel parsing (Rust + tree-sitter, TS vs TSX vs Python grammars per extension). - AST extraction — functions, arrows, function expressions, classes, methods, variables, enums, interfaces, type aliases, namespaces, with byte offsets (for edits) and 1-based lines (for output).
- Symbol table — stable IDs (
file::kind:name, full parent chains so shadowed names never collide); lexical-scope resolution (nearest declaration wins; class scopes excluded from bare-name lookup). - Import resolution (highest-priority correctness area) — relative +
extensionless + directory index,
tsconfig.jsonbaseUrl/paths, barrel files (export *, named re-exports, chains), default/namespace/ aliased imports, CommonJSrequire(incl. destructured),module.exportsforms,export =, package self-imports,/// <reference path>via declaration files, subpackagepackage.jsonentries (monorepos). Python resolves dotted specifiers against packages (pkg/mod.pyvspkg/mod/__init__.py), relative./..levels, and flat/src/monorepo roots. - Value vs type edges —
import typecreatesTYPEedges; computed type keys, heritage clauses, annotations, and generics included. - Reference extraction — calls,
new, identifiers, member calls with safe receivers (thisvia scope walk,new X()/typed vars/class names/ object owners/namespaces), JSX components,this.method(),super(via heritage), object-shorthand uses, computed member reads. - Dynamic-use detection —
import(expr),require(expr),import.meta.resolve(expr),path.join/resolve(__dirname, …),eval,Reflect.*, global computed access,obj[key]()dispatch, directory scans (readdir+require); in Pythonimportlib.import_module,__import__,getattr,eval/exec,__subclasses__andpkgutil.walk_packages. Unresolvable dynamic loading protects nearby files (prefix dirs) instead of pretending. - Entry points —
diet-code.jsonfirst, thenpackage.json(main/module/browser/exports/bin/types, per package in monorepos),pyproject.toml/setup.pyconsole scripts and declared packages, then conservative conventions (src/main.*,src/index.*,__main__.py,__main__guards, …). Tests (test/,*.test.*,test_*.py, …) form a separate root set. - Reachability — file BFS from production ∪ public-surface ∪ script-referenced entries; symbol BFS from production entry symbols (+ top-level statements). Records production/test reachability separately.
- Findings + confidence + Git evidence (supporting only).
Every finding lists checkable facts: zero incoming references, unreachable from entry points, zero importers, no package export, no configuration reference, no dynamic-reference evidence. There are no invented scores.
Confidence is explicit, never numeric:
| Level | Meaning | Auto-removed? |
|---|---|---|
CERTAIN |
unexported, zero refs, unreachable, no dynamic risk | ✅ yes |
HIGH |
zero production consumers, no entry/config/dynamic risk (test-only refs allowed) | ✅ yes (verify decides) |
MEDIUM |
looks unused but external/dynamic use can't be ruled out (public surface, plugins, protocols, dispatch) | ❌ never |
LOW |
significant uncertainty (dynamic loading, tooling configs, scripts, ambiguous surface) | ❌ never |
Conservative by construction: exported symbols, package surfaces, tooling
configs (babel/webpack/jest/karma/…), script-referenced files, CI
workflow references, test-runner magic (__mocks__), agent-tooling dirs,
minified files, ambient/global types, plugin/prototype registration,
callback/hook objects, computed dispatch, inheritance hierarchies
(super, overrides share liveness), declaration merging, and
object spread are all capped at MEDIUM/LOW or skipped. What remains
CERTAIN survived all of that.
Python has no export keyword and no static types, so the same facts are read
through Python's own rules rather than the ES-module ones:
| Python fact | How it is treated |
|---|---|
module-level name without a leading _ |
importable by any consumer → MEDIUM at best, never auto-removed |
_name / __name |
private by convention → can reach CERTAIN |
__all__ |
declared import * surface; additive only (a name absent from it is still importable) |
import pkg.mod |
also runs pkg/__init__.py, so package initialisers are reachable |
pkg/__init__.py |
the package's import surface — reportable, never auto-removed |
if __name__ == "__main__":, __main__.py, manage.py, wsgi.py/asgi.py |
program entry points that root the import graph |
[project.scripts] / console_scripts |
installed commands are production roots |
__init__, __enter__, and other dunders |
interpreter protocols, never dead-code candidates |
@staticmethod, @property, @dataclass, … |
transformers: ordinary rules still apply, so unused ones are still reported |
| any other decorator | the definition is handed to a callable we cannot follow → treated as used |
test_*.py, *_test.py, conftest.py |
test scope, like *.spec.ts |
importlib.import_module(f"pkg.{n}"), __import__("pkg." + n) |
the static prefix protects that directory |
getattr, eval/exec, __subclasses__, pkgutil.walk_packages |
recorded as dynamic risk, capping confidence |
Two Python-only safety rules exist because Python is whitespace-significant and duck-typed:
- Nested declarations are never auto-removed. Deleting the only statement
of a
class,def, orexceptblock leaves a syntax error, so only declarations starting at column 0 are eligible for automatic removal. - Python imports are never pruned.
import pkgis an executable statement whose side effects (registering models, codecs, plugins) can be the reason it is there, so removing an apparently unused one is not provably safe.
Byte-range deletions from tree-sitter offsets (full lines only, no
reformatting), git rm for dead files, mechanically-proven unused-import
pruning only (ES modules; see Python specifics above). Before touching
anything: clean-tree check → record HEAD → new branch → apply → re-analyze →
run discovered verify scripts (typecheck/test/build via the repo's
package manager, or verifyCommands) → revert on failure unless --keep.
Same prompt, same base commit, same model, fresh directory + fresh session
per run, same verify command, no findings leaked into prompts, no human
intervention, randomized run order (base/diet/diet/base/…), ≥3 reps
recommended (medians reported with min–max ranges). DIET differs from BASE
only by the deterministic cleanup — no AI cleanup, ever. Success comes
only from verify exit codes.
- Small DIETs on mature repos. Well-maintained projects yield little
auto-removable product code (linters already catch unused locals). Our
gotDIET was 3 files / 171 LOC (a benchmark harness server + 2 docs examples). Effect sizes scale with diet size; do not extrapolate. - Scope. v0.1 cannot price the value of docs/examples; they count as
structure.
diet-code.jsoninclude/excludeis the scoping mechanism. - Variance. Free-tier models are slow and noisy (identical task/branch runs differed by 2–4× in tokens/time). Treat single-rep deltas as noise; use ≥3 reps and read the ranges.
- Token accounting. We sum fresh input + cache reads/writes (comparing
raw
inputalone would reward warm caches, not smaller repos). - Time is dominated by model latency, not repo size — it is reported, not claimed as an effect.
- No causality beyond the experiment. Numbers describe these tasks, this repo, this model, this day.
Subject: sindresorhus/got @ 687eb7d
(85 files analyzed). Diet Code removed 3 files / 171 LOC with zero
MEDIUM/LOW touched; existing tests (headers, retry, hooks:
282 passed) stayed green. Three feature tasks
(throwHttpErrors array form, response.redirected, URLSearchParams
body), reps=1 per arm (pilot scope — see limitations), fresh session +
fresh clone per run, randomized order.
| Task | BASE | DIET | |
|---|---|---|---|
| throw-http-errors-array | Input tokens | 66K | 45K (−31%) |
| Tool calls | 12 | 8 (−33%) | |
| Files read | 9 | 6 (−33%) | |
| Time | 585s | 420s | |
| Success | 1/1 | 1/1 | |
| response-redirected | Input tokens | 74K | 155K (+111%) |
| Tool calls | 7 | 12 (+71%) | |
| Files read | 4 | 3 (−25%) | |
| Time | 580s | 229s | |
| Success | 1/1 | 1/1 | |
| urlsearchparams-body | Input tokens | 34K | 71K (+106%) |
| Tool calls | 6 | 5 (−17%) | |
| Files read | 5 | 2 (−60%) | |
| Time | 421s | 80s | |
| Success | 1/1 | 1/1 |
Reading: files-read was lower on DIET in all 3 tasks; tokens/tool calls
went both ways (run-to-run variance at reps=1 dominates — same task/branch
varied 2–4× across runs earlier); success unchanged 6/6. With reps=1 this
is a pilot, not a verdict: it validates the harness end-to-end (real
implementations, real passes) but cannot confirm or reject the thesis.
Full report: benchmarks/got/benchmark-2026-09-18-mimo-r1.json.
During development the analyzer was also validated against 15 real
repositories (dayjs, zod, commander, cheerio, marked, yargs, hono, execa,
got, axios, mocha, eslint, shelljs, ofetch, uptime-kuma, excalidraw,
TriliumNext): precision was driven up until zero-removable on clean repos
(dayjs, zod, shelljs, …) with no false CERTAINs found by audit, rather
than chasing recall.
[CERTAIN] dead_file benchmark/server.ts
✓ 0 production importers ✓ not a package entrypoint
✓ not configured as an entrypoint ✓ no supported dynamic reference
[CERTAIN] dead_variable src/v4/core/compile.ts:707-checkFn
✓ no references ✓ not exported ✓ not reachable ✓ no dynamic usage
diet-code explain src/v4/core/compile.ts:707-checkFn
# Classification: DEAD FUNCTION / Confidence: CERTAIN + evidence + git historycrates/diet-code-core/src/
lib.rs orchestration: discover → parse → resolve → graph →
entrypoints → reachability → findings → git evidence
parser.rs tree-sitter extraction (TS/TSX/JS/JSX), imports, refs,
dynamic-use signals, escape/publication signals
python.rs Python extraction into the same ParsedFile shape: dotted
module resolution, __all__, packages, decorators, dunders
symbols.rs Entity model (stable IDs, byte offsets)
imports.rs import / re-export / reference records (VALUE vs TYPE)
resolver.rs relative, tsconfig paths, barrels, CJS, self-imports
graph.rs file + symbol graphs, lexical-scope + receiver resolution
entrypoints.rs diet-code.json, package.json(s), pyproject/setup.py,
conventions, test sets, tooling/script/workflow/config guards
reachability.rs BFS from production ∪ public ∪ script roots (+ test roots)
findings.rs finding kinds + stable JSON schema (version 1)
confidence.rs CERTAIN/HIGH/MEDIUM/LOW (+ auto-removable rule)
edits.rs deterministic byte-range removals + import pruning
crates/diet-code-cli/src/
main.rs commands/{analyze,explain,clean,benchmark,install}.rs output.rs
skills/diet-code/SKILL.md the `/diet-code` agent skill (embedded by `install`)
fixtures/ 25 fixture repos × expectations (cargo test)
benchmarks/got/ real-repo task pack + measured report
Core never touches the terminal, Git, or the network; the CLI owns I/O.
Agent telemetry is isolated behind trait AgentRunParser (opencode +
claude impls; Codex/Gemini/Cursor later).
- Precision first. A new finding kind must come with fixtures proving
both sides (detected when dead, silent when live), especially the
framework/dispatch/dynamic patterns in
fixtures/. - Never auto-remove on suspicion. If external, dynamic, or
framework-driven use is possible, cap at
MEDIUM/LOWwith a reason. - Deterministic only in core. No network, no models, no clocks in
analysis results.
cargo test --workspacemust pass offline (thefossil-mcpdifferential test is#[ignore]d and optional). - Benchmark honesty. Same prompt/commit/model/session-freshness, randomized order, verify-decides-success, report ranges, document limitations. A negative result is a result.
cargo test --workspace # all suites, offline
cargo install --path crates/diet-code-cli
diet-code analyze .