Skip to content

Using `crimes` with coding agents

crimes was designed for AI coding agents (Claude Code, Cursor, Codex, Copilot Workspace, Aider, OpenAI agents, etc.) operating in unfamiliar codebases. The --format json output is the stable contract that agents should consume — the human-readable report is a rendering of the same underlying findings.

This document covers:

  • Pre-edit briefing with crimes context — the recommended first step
  • Scan and post-edit gates — crimes scan and crimes scan --changed
  • Triage — crimes triage for per-finding disposition + reason + owner
  • Verdict — one-line branch summary
  • Supporting commands — crimes hotspots, crimes diff, crimes baseline
  • How to interpret findings as an agent
  • What is guaranteed, what may change, and what is not implemented yet

If you are looking for the wire format itself, read docs/json-schema.md. For the catalogue of bundled agent instructions (root AGENTS.md, Claude Code skill), see docs/skills.md.


JSON for decisions, human output for readbacks

Section titled “JSON for decisions, human output for readbacks”

crimes ships two output modes for the same underlying findings. Agents should treat them as different tools for different jobs:

  • --format json is for agent decisions. Use it whenever you are planning an edit, gating CI, diffing two refs, or comparing finding counts before / after a change. JSON is the stable contract — the shape is documented in json-schema.md and versioned by schema_version.
  • Default human output is the canonical user-facing readout. When the user wants to see the results, or you are summarising findings back in chat, rerun the same command without --format json and paste the relevant section. Severity glyphs, file groupings, evidence, feedback hints, suppressions, and gate status are all already rendered there for people to read.

Human output is not the schema contract — its wording, glyphs, and layout can shift between minor releases. Don’t parse it. But do treat it as the canonical presentation: rather than paraphrasing the whole JSON in your own voice, quote the human readout and add interpretation only around the parts that matter for the task at hand.


crimes ships with two on-disk artefacts that coding agents pick up automatically when they open a repo that contains them. You do not need to copy-paste anything into a prompt — they are loaded by the agent itself.

A skill lives at .claude/skills/crimes/SKILL.md. Claude Code discovers any SKILL.md under .claude/skills/<name>/ and loads it when the user invokes the matching skill (e.g. /crimes or when Claude judges the skill relevant). The skill is short by design: it tells Claude when to run crimes, which command to use, and how to read the JSON.

Recommended Claude Code prompt fragments, if the user wants to add a project-level reminder in CLAUDE.md:

Before editing any file in this repo, run crimes context <file> --format json and read every high severity finding. After editing, re-run the same command or crimes scan --changed --format json and treat any new high finding as a blocker.

The root AGENTS.md covers install / build / test / architecture / safety rules — Claude Code reads AGENTS.md as well, so you get both layers (general agent rules + the on-demand skill).

AGENTS.md at the repo root is the convention Codex CLI (and Aider, Cursor, Copilot Workspace, OpenAI agents, etc.) read on startup. It contains:

  • install / build / test commands,
  • the four shipped crimes commands and their flags,
  • project architecture and package boundaries,
  • coding style notes,
  • agent safety rules (no auto-publish, no shared-branch rewrites, no silent auto-fix of findings).

For Codex, the pre-edit / post-edit loop is the same as the rest of this document — invoke crimes context <file> --format json before touching a file, crimes scan --changed --format json after, and diff the findings.

Other agents (Cursor, Aider, Continue, etc.)

Section titled “Other agents (Cursor, Aider, Continue, etc.)”

Anything that reads AGENTS.md or CLAUDE.md will pick up the workflow without further configuration. For agents that read neither, point them at this document or copy the Pre-edit briefing and Post-edit gate sections into your project’s agent-rules file.

crimes init --agents (since 0.11.0) writes a Claude Code PreToolUse Edit hook in addition to the SKILL.md files. The hook runs crimes context --format json on the file being edited so the agent gets a structured pre-edit briefing without having to remember to invoke the command.

Files written:

PathBehaviour
.claude/skills/crimes/SKILL.mdUnchanged (since 0.9.0).
.agents/skills/crimes/SKILL.mdUnchanged (since 0.9.0).
.claude/settings.local.jsonNew. Merge-write of a PreToolUse Edit hook (see below).
.agents/settings.local.jsonNew (placeholder). Same JSON shape; Codex does not honour PreToolUse yet.

The hook itself (merged into .claude/settings.local.json, never overwritten wholesale):

{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "npx -y crimes hook --format compact || true",
"timeout": 8000
}
]
}
]
}
}

Contract details:

  • Matcher is Edit|Write|NotebookEdit — the hook fires on every Claude Code tool invocation that mutates a file.
  • Command runs crimes hook --format compact, which reads the hook JSON from stdin and prints a short pre-edit crimes context briefing for the target path. The generated shell command appends || true, so failures never block the edit; unexpected hook errors still emit a short stderr warning so setup problems are discoverable.
  • Timeout is 8 seconds.
  • Merge semantics. crimes init --agents reads the existing .claude/settings.local.json, parses it, appends the entry to hooks.PreToolUse[], and writes back (2-space indent, trailing newline). If the file already contains a crimes hook (detected by the crimes hook or legacy crimes context substring in command), the write is a no-op. A malformed existing file exits 2; pass --force to overwrite.
  • --no-hooks opts out of writing either settings file. The SKILL.md files still ship.

Codex stub. .agents/settings.local.json carries the same JSON shape as the Claude file and uses the same stdin-reading crimes hook command. Codex does not honour PreToolUse hooks as of crimes@0.11.1; the file is a forward-looking placeholder so the schema is ready when the Codex hook surface lands. A top-level _note key documents this. Safe to delete.

The hook is non-optional by default because the briefing is the load-bearing piece — an agent that never calls crimes context before editing gets none of the value. If your team prefers to opt out, run crimes init --agents --no-hooks or delete the settings file manually.


SituationCommand
About to edit one specific filecrimes context <file> --format json
About to refactor across a directorycrimes scan <path> --format json
Mid-task, want to re-check only the files you have touchedcrimes scan --changed --format json
Reviewing a feature branch before mergecrimes scan --changed --base main --format json
Bulk-handling a backlog of findings on a legacy repocrimes triage (or crimes triage --apply <file> in CI)
One-line “did this branch help or hurt?” summarycrimes verdict --format json
Comparing two committed refs (e.g. main vs HEAD)crimes diff main...HEAD --format json
Gating CI on “no new debt vs the saved baseline”crimes baseline check --format json
Adopting crimes on a legacy repo (snapshot, then commit)crimes baseline save
Triaging “where in the repo is the most change-risk right now?”crimes hotspots --format json
Reviewing the whole repo from scratchcrimes scan . --format json --all

If you only learn one command, learn crimes context <file> --format json — it is the cheapest, most file-specific entry point.


Start here. Before touching any file, run crimes context on it:

Terminal window
crimes context path/to/file --format json

This is the single highest-leverage command in an agent loop. It returns the per-file findings, the test files most likely to cover the target, the files in the neighbourhood an agent should read first, and short safe-editing notes — all deterministic, no LLM, no git history required.

If you are running against an unreleased checkout, invoke it from this monorepo as node packages/cli/dist/index.js context path/to/file --format json.

The JSON shape is (canonical key order — agent_guidance first):

{
"schema_version": "0.1.0",
"report_type": "context",
"file": "src/billing.ts",
"risk": { "level": "high", "high": 1, "medium": 1, "low": 1, "total": 3 },
"agent_guidance": [
"Prefer extracting pure helpers before adding more branches.",
"Avoid adding more direct clock access; inject time where possible."
],
"clues": {
"churn": {
"commits_90d": 14,
"last_commit_at": "2026-05-18T12:30:00Z",
"unique_authors_90d": 3
},
"suppressions": [
{
"fingerprint": "large_function::src/billing.ts::renderInvoice",
"detector": "large_function",
"reason": "Legacy billing module, rewrite planned",
"pinned_version": "0.9",
"matches_current_finding": false
}
],
"test_gap": {
"raw": 1,
"percentile": 0.85,
"label": "top-quartile"
},
"related_signals": []
},
"related_files": [
{
"file": "src/nav/sidebar.ts",
"reason": "related to Route Metadata Drift",
"score": 0.4
},
{
"file": "src/billing-policy.ts",
"reason": "shares domain token \"billing\"; matches domain \"billing\"",
"score": 0.4
}
],
"likely_tests": ["src/billing.test.ts"],
"findings": [ /* same Finding shape as scan */ ]
}

How to use the fields (read in this order):

  • risk.level is the headline (none | low | medium | high) — the worst severity present on this file.
  • agent_guidance is one short line per finding type that fired, deduped. Read it first — it tells you what not to make worse before you read anything else. When the file has no findings but does have related files, you’ll instead see one line pointing you at the neighbourhood.
  • clues (new in 0.10.0) is an optional object with up to three sub-blocks: clues.churn carries commits_90d / last_commit_at (ISO 8601) / unique_authors_90d and is omitted when git is unavailable; clues.suppressions is a per-file inventory of every suppression entry that scopes to this path (each entry carries fingerprint, detector, reason, pinned_version, matches_current_finding) and is omitted when empty; clues.test_gap carries the raw {0, 0.5, 1} value plus a repo-relative quartile percentile and a human label ("top-quartile" / "median" / "bottom-quartile" / "unknown"). clues.related_signals is always present as [] — reserved for future use. Frozen contract — safe to consume from a PreToolUse hook.
  • related_files is a ranked, capped list (max 10) of other files in the repo that an agent should probably read before editing the target. Each entry carries a reason (related to <charge>, shares domain token "<token>", matches domain "<token>", or same directory) and a score sort key. Source-of-truth files for the same concept usually surface here — the labelled nav source for a route, the helper module for an API handler, the sibling pages in the same flow. Read these before editing.
  • likely_tests is found by four deterministic conventions: same-basename .test.ts / .spec.ts siblings, Go-style _test.ts / _spec.ts siblings, files under __tests__/ matching the basename, and test files that import the target via a relative path. Run these tests after editing.
  • findings are the same Finding objects crimes scan would emit, filtered to this file. Read every high first.

When any of agent_guidance, related_files, or likely_tests is the empty array, the corresponding *_reason field is set to a short string explaining why (e.g. "no neighbourhood signal: …"). Treat [] plus a reason as “we searched and found nothing”, not as “we didn’t search”.

When the change spans more than one file, run a scoped directory scan instead of crimes context on each file individually:

Terminal window
crimes scan path/to/dir --format json

What to do with the result:

  • Read every high severity finding in findings[] before you write code. Each one is concrete evidence that this area is risky to touch.
  • Treat evidence as ground truth. It is generated deterministically from the AST or file contents — not from an LLM.
  • Use lines + symbol to focus reading on the exact functions or ranges the detector flagged, instead of re-reading the whole file.
  • Use suggested_actions[].kind as a hint for the kind of change the detector thinks is safe. They are heuristics, not instructions — pick the ones that match the user’s request.

After making a change, re-scan to see whether you introduced new findings. The two most useful post-edit gates are:

crimes scan --changed (narrow gate, preferred)

Section titled “crimes scan --changed (narrow gate, preferred)”

crimes scan --changed restricts the scan to files changed in the working tree (staged, unstaged, and untracked), optionally including everything that differs between a base ref and HEAD. This is the cheapest scope when you are mid-task and do not need to re-scan an entire directory:

Terminal window
crimes scan --changed --format json # working tree only
crimes scan --changed --base main --format json # + commits on this branch
crimes scan --changed --base origin/main --format json # + commits not yet pushed
crimes scan --changed --fail-on high --format json # CI gate — exit 1 on a new high

Semantics:

  • With no --base, staged, unstaged, and untracked files are all included.
  • With --base <ref>, the additional set is <ref>...HEAD — i.e. commits unique to the current branch since it diverged from <ref>.
  • Deletions are skipped — the file is gone, so there is nothing to scan.
  • Non-source files in the changed set (Markdown, JSON, lockfiles, etc.) are filtered out via the configured include / exclude patterns.
  • Outside a Git repository the command exits 2 with a clear error on stderr; the JSON output is not produced. Agents should fall back to a path-scoped crimes scan <path> when this happens.

The output is the same ScanReport shape as crimes scan — same schema_version, same finding fields — just over a smaller set of files.

crimes scan --changed also populates a top-level changed_files array on the JSON output, listing every file the resolver returned (repo-relative POSIX, sorted, deduped) — including files that produced zero findings (a touched README.md, a package.json bump, a .ts file the detectors had nothing to say about). The field is absent on plain crimes scan. Read it when you need to confirm what your edit actually touched, even when the report is clean:

{
"schema_version": "0.1.0",
"report_type": "scan",
"summary": { "total": 0, "high": 0, "medium": 0, "low": 0 },
"findings": [],
"changed_files": [
"README.md",
"src/billing.ts",
"src/billing.test.ts"
]
}

--fail-on (CI gate, opt-in):

  • Valid only in combination with --changed. Passing it on a plain crimes scan exits 2 (usage error).
  • Accepts low | medium | high. low fails on any finding; medium fails on medium or high; high fails on high only.
  • When set, the ScanReport JSON gains two optional top-level fields: fail_on (the threshold the run gated on) and failed (a boolean that flips to true when at least one finding in the changed set meets the threshold). Both fields are absent when --fail-on isn’t passed — crimes scan --changed without the flag is unchanged.
  • Exit 1 when failed is true; exit 0 otherwise. The human output ends with an OK: / FAILED: line summarising the gate.

Decision rule for agents: when failed is true after your edit, treat it the same as a new high-severity finding in crimes diff — fix or surface the cause before completing the task. See docs/ci.md for the CI-side equivalent of this gate.

A directory scan is most useful at the start of a multi-file task, or when you want to understand the overall risk profile of a subtree:

Terminal window
crimes scan . # file-grouped, top 5 files (default)
crimes scan . --top 10 # show top 10 files
crimes scan . --flat # revert to severity-grouped output
crimes scan . --all # every finding across every file
crimes scan . --no-recency # disable recency weighting in ranking
crimes scan . --format json # stable JSON contract

The default human output groups findings by file, sorted by aggregate risk (churn × test-gap quartile × blast radius × recency). Each file header shows the finding count, high/medium tally, and a one-line Risk summary. Use --flat if you want the old severity-grouped list; use --all to see every finding without a file cap.

Decision rule (same for all scan commands):

  • If your edit introduced a new severity: "high" finding, treat it as a blocker — either fix it before continuing, or surface it to the user with a short justification.
  • If agent_risk increased on a touched file, slow down: you may have added a hidden source of truth, a duplicate rule, or a misleading name.
  • If the total counts in summary went down, you are in a good state.

When a scan turns up more findings than you intend to address in one edit, the recommended path is crimes triage, not crimes baseline save. Triage is per-finding: each entry carries a disposition + reason + owner + date instead of bulk amnesia.

Terminal window
crimes triage # interactive walk over current findings
crimes triage --apply dispositions.json # non-interactive (scripted use)
crimes triage --list # show current entries; no scan, no prompts
crimes triage --retriage src/billing.ts # re-open the prompt for matching entries
crimes triage --clear <fingerprint> # remove one entry

Five dispositions:

DispositionDefault crimes scanResurfaces on touched files?
fix-nowshown ( prefix)n/a
fix-this-PRshown ( prefix)n/a
needs-designhiddenyes — “still intentional?”
wont-fixhiddenyes — “still intentional?”
scaffoldinghiddenyes — “still intentional?”

Entries persist to .crimes/triage.json, intended to be committed. The schema is the same <type>::<file>::<symbol-or-empty> fingerprint as baseline + suppressions; small line shifts don’t invalidate an entry. reason, owner, and date are required at the schema level.

Resurfacing. needs-design, wont-fix, and scaffolding are silenced by default but resurface automatically when the file appears in the branch diff against config.triage.resurfaceBase (default "main"). The resurfaced finding carries previously_triaged: true plus a previous_triage block (the original disposition + reason + owner + date) and renders with a ▼ was previously triaged annotation in the human report. Baseline entries get the same treatment with previously_baselined / previous_baseline. Set triage.resurfaceBase: "" in crimes.config.json to disable resurfacing.

New crimes scan flags introduced for triage / resurface:

FlagDefaultEffect
--show-triagedoffInclude silenced dispositions in output (annotated with hidden_triage).
--gate-needs-designoffWhen --fail-on is set, count needs-design findings toward the gate.
--gate-resurfacedoffWhen --fail-on is set, count resurfaced findings on touched files toward the gate.

Default gate semantics: wont-fix, scaffolding, and needs-design are excluded from --fail-on (the user already triaged them). Resurfaced findings are surfaced as a reminder, not a block. Opt in with the flags above when your team wants stricter gating.

Decision rule for agents:

  • When crimes scan shows a resurface block (previously_triaged or previously_baselined findings), read each entry and decide whether the original disposition still holds. If it does, leave the entry; if it doesn’t, run crimes triage --retriage <file> to re-open the prompt.
  • Don’t reach for crimes baseline save to silence a noisy first scan. Run crimes triage and walk the list — the receipts (reason / owner / date) are the audit trail the team will need when the file changes again.

When the agent finishes a task and wants a one-line “did this branch help or hurt the repo?” answer, run crimes verdict. It is built on top of crimes diff, so it inherits the same fingerprint matching and working-tree-safety, but emits a single headline verdict instead of the full new/fixed/unchanged breakdown:

Terminal window
crimes verdict --format json # default base: origin/main → main
crimes verdict --base main --format json # override base
crimes verdict --fail-on new-high # opt-in CI gate (exit 1)

Default base selection: origin/main first, then main. If neither resolves the command exits 2 with a “no default base” error on stderr; pass --base <ref> explicitly.

The JSON shape:

{
"schema_version": "0.1.0",
"report_type": "verdict",
"repo": { "name": "...", "root": "..." },
"base": "origin/main",
"head": "HEAD",
"verdict": "worse",
"summary": {
"new": 2, "fixed": 1, "unchanged": 8,
"new_by_severity": { "high": 1, "medium": 1, "low": 0 },
"fixed_by_severity": { "high": 0, "medium": 1, "low": 0 },
"new_weighted": 5,
"fixed_weighted": 2
},
"reasons": ["introduced 1 high-severity crime"],
"recommended_actions": ["fix new high-severity findings before merging."],
"new_findings": [ /* same Finding shape as crimes scan */ ],
"fixed_findings": [ /* ... */ ]
}

How to use the fields:

  • verdict is the headline: one of "cleaner" | "worse" | "unchanged" | "mixed". Read this first.
  • reasons is a short array of human-readable strings explaining what drove the verdict. Quote them back to the user when summarising.
  • recommended_actions is one or two lines suggesting next steps. Treat both reasons and recommended_actions as advisory copy — wording may shift across minor releases.
  • summary.new_weighted / summary.fixed_weighted are the simple weighted scores (high = 3, medium = 2, low = 1) that drive the judgement. Treat as ordinal — exact weights may change.
  • summary.new_by_severity / summary.fixed_by_severity are the per-severity counts on each side, useful when explaining the trade-off.
  • new_findings[] / fixed_findings[] carry the full Finding shape — same contract as crimes diff. Quote evidence and lines when explaining what changed.

Judgement rules, in order:

  1. unchanged — no new and no fixed findings.
  2. worse — any new finding has severity: "high".
  3. worsenew_weighted > fixed_weighted (no new high required).
  4. cleanerfixed_weighted > new_weighted AND no new high.
  5. mixed — both sides non-zero with equal weighted scores.

Exit codes:

--fail-onExit 1 when …
(omitted)Never — crimes verdict is advisory by default.
worseverdict === "worse".
new-highAny new finding has severity: "high".
new-mediumAny new finding has severity: "medium" or "high".

Exit 2 is reserved for usage / environment errors (not a git repo, no default base resolves, bad flag).

Decision rule: when verdict === "worse" because of a new high finding, the agent should treat that as a blocker — same rule as the rest of this document. When the verdict is mixed, surface the trade-off rather than silently merging.


If you are leaving findings in the code on purpose, say so in your PR description or chat reply, citing the finding id and charge. Do not silently suppress findings.

Good:

Leaving crime_00001 (God Function on generateInvoice) as-is for this change — splitting it is out of scope for the bugfix you asked for.

Bad:

Done.


crimes hotspots — where in the repo is change-risk highest?

Section titled “crimes hotspots — where in the repo is change-risk highest?”

crimes hotspots uses git history to rank files by change-risk; on a shallow clone (common in CI runners that default to --depth=1) the report sets an optional top-level history_limited: true plus a short history_limited_reason. When you see those, treat the ranking as advisory — older commits are missing from the local copy. Deepen the clone (fetch-depth: 0 in GitHub Actions) to clear the flag.

Terminal window
crimes hotspots --format json
crimes hotspots --since 90d --format json
crimes hotspots --all --format json

JSON output is capped to the top 20 rows by default, matching the human report. Check hidden_count before assuming the payload is complete; pass --all --format json when you need every file.

crimes diff — new / fixed / unchanged findings between two refs

Section titled “crimes diff — new / fixed / unchanged findings between two refs”

When you have two committed refs and want the deltas — e.g. reviewing what a feature branch did vs main, or what landed on main between two releases — use crimes diff:

Terminal window
crimes diff main...HEAD --format json
crimes diff origin/main...HEAD --format json
crimes diff v0.1.0...HEAD --format json

The range must be the triple-dot form (<base>...<head>).

crimes diff is working-tree-safe: it exports each ref into a fresh temp directory via git archive and scans it there. No checkout, no stash, no temporary commits — your dirty working tree is preserved.

The JSON shape:

{
"schema_version": "0.1.0",
"report_type": "diff",
"repo": { "name": "...", "root": "..." },
"base": "main",
"head": "HEAD",
"summary": { "new": 2, "fixed": 1, "unchanged": 8 },
"new_findings": [ /* same Finding shape as crimes scan */ ],
"fixed_findings": [ /* ... */ ],
"unchanged_findings": [ /* ... */ ]
}

How to use the fields:

  • summary.new is the headline gate: if it is > 0 and any of those findings are severity: "high", treat the branch as introducing regressions and either fix or surface them to the user.
  • new_findings[] carry the full Finding shape — quote evidence and lines back when explaining what changed.
  • fixed_findings[] are wins. Mention which charges this branch cleared when summarising the work.
  • unchanged_findings[] are pre-existing debt. Don’t relitigate them in the diff conversation — they were there before the branch.

How findings are matched across the two refs: stable fingerprint <type>::<file>::<symbol-or-empty>, not the per-scan id. Small line shifts from unrelated edits do not register as fix + new. See docs/json-schema.md for the full fingerprint rationale and known limitations (file renames register as fix + new, identical-name nested helpers collide on one fingerprint).

Decision rule, same as the rest of the workflow: a new severity: "high" finding in new_findings is a blocker unless the user explicitly accepts the risk.

crimes baseline — gating CI on the saved baseline

Section titled “crimes baseline — gating CI on the saved baseline”

When the repo has a committed .crimes/baseline.json, the agent loop can run the same gate CI uses:

Terminal window
crimes baseline check --format json
crimes baseline check --fail-on high --format json # stricter gate

crimes baseline save writes the snapshot; crimes baseline check compares the current scan to that snapshot and fails on findings absent from the baseline. Adoption flow:

Terminal window
crimes baseline save # one-time per repo
git add .crimes/baseline.json
git commit -m "Add crimes baseline"
# … later, on every PR …
crimes baseline check

The JSON shape:

{
"schema_version": "0.1.0",
"report_type": "baseline_check",
"repo": { "name": "...", "root": "..." },
"baseline_path": "/abs/path/to/.crimes/baseline.json",
"fail_on": "medium",
"failed": false,
"summary": {
"total_baseline": 5, "total_current": 5,
"new": 0, "fixed": 0, "unchanged": 5,
"new_by_severity": { "high": 0, "medium": 0, "low": 0 }
},
"new_findings": [ /* same Finding shape as crimes scan */ ],
"fixed_findings": [ /* BaselineEntry — fingerprint + identity fields */ ],
"unchanged_findings": [ /* same Finding shape as crimes scan */ ]
}

How to use the fields:

  • failed is the gate. true → at least one new finding meets the --fail-on threshold; exit 1. false → exit 0.
  • new_findings[] carry the full Finding shape. Quote evidence and lines when explaining what’s new vs the baseline.
  • fixed_findings[] are BaselineEntry records (fingerprint + type
    • charge + severity + file + symbol). Mention which charges this branch retired. The minimal shape is intentional — once a finding is fixed, its old lines / evidence no longer make sense.
  • unchanged_findings[] are the legacy debt the baseline pins. Don’t relitigate them in the conversation — they were already accepted when the baseline was committed.

--fail-on thresholds:

ValueA new finding fails when its severity is …
"low"low, medium, or high
"medium"medium or high (default)
"high"high only

Exit codes:

ExitMeaning
0No new findings at or above --fail-on.
1At least one new finding at or above --fail-on — blocking.
2Missing or malformed .crimes/baseline.json, or a bad flag.

Findings are matched by the same stable fingerprint <type>::<file>::<symbol-or-empty> as crimes diff — small line shifts don’t register as fix + new. See docs/json-schema.md for the full schema and known limitations.


crimes@0.3.0 adds five information architecture detectors that look for ambiguous sources of truth across the repo:

Finding.typeChargeReads
missing_agent_contextMissing Agent ContextAGENTS.md / CLAUDE.md / .claude/skills/*/SKILL.md / .agents/skills/*/SKILL.md + package.json bin
route_metadata_driftRoute Metadata DriftRoute file paths, default exports, <title> / metadata.title, and nav-source labels
duplicated_navigation_sourceDuplicated Navigation SourceTop-level nav-array literals across files
concept_alias_driftConcept Alias DriftPath tokens, route paths, labels, nav entries, and doc headings
docs_code_driftDocs-Code DriftLocal links in docs/**/*.md and root-level *.md

IA findings are cross-file by design. The file field anchors the finding on the most useful single path (the route file, the lexicographically first nav source, the alias-group anchor, the doc), and related_files lists the other repo-relative paths that contributed evidence. Treat related_files as “also read these before editing” — same scope as the finding itself. The human reporter renders those paths as an “Also touches:” block under each finding (capped at 5 with the rest summarised), so a grep-friendly run still surfaces every file an agent needs to read.

Why this matters for agents. Without IA findings, an agent asked to “rename the billing page” or “update the team copy” picks the file it greps first and leaves every other source of truth stale. IA findings make that ambiguity visible before the edit — every related_files entry is a place where the same concept lives under a different label, in a different file, with a different vocabulary. Read them before choosing which one is canonical.

Each agent_guidance line is keyed on Finding.type:

TypeGuidance
missing_agent_contextAgents may miss project-specific commands, architecture rules, and safety checks.
route_metadata_driftThe route path, title, breadcrumb, and component name appear to disagree — verify each before changing labels.
duplicated_navigation_sourceMultiple files declare this destination; updating only one will leave the others stale.
concept_alias_driftOther files describe this concept under a different name; read them before renaming or extending.
docs_code_driftDocs reference local files that no longer exist — update the docs in the same PR.

Key contract. IA findings are ambiguity signals, not claims of semantic truth. The detector phrases its summary as “appears to” / “may” / “looks like” — and so should you when relaying a finding to a user. Every evidence string is concrete (file path, line, literal value); the interpretation of that evidence belongs to the human or agent reading the report.

No LLM, no API key, no network access is required to produce these findings. Every IA detector runs as a deterministic AST + markdown pass. Two runs over the same repo produce identical IA findings.

For long-form per-detector reference (false-positive notes, quorum rules, suggested fixes), see docs/finding-types/ia.md.


Petty crimes are small, local maintainability signals that make code easier to misread. They are not style lint. They exist because coding agents copy nearby patterns, trust comments, and infer safety from names.

Finding.typeChargeAgent concern
commented_out_codeCommented-Out CorpseDisabled code can look like reusable implementation or current documentation.
logic_in_commentsLogic in the AlibiA business rule may live only in prose instead of guards, types, config, or tests.
name_behavior_mismatchFalse IdentityA safe-sounding function may write, send, track, charge, or otherwise mutate state.
magic_domain_literal_scatterString SprinklesRepeated domain strings invite another copy instead of a source-of-truth edit.
weak_test_signalTest That Proves NothingA present-but-weak test can make an edit look safer than it is.
option_bag_junk_drawerOption Bag Junk DrawerGeneric bags hide which fields an edit must preserve.
return_shape_rouletteReturn Shape RouletteDivergent anonymous return shapes make callers infer the wrong contract.
negative_flag_mazeNegative Flag MazeMultiple negative flags are easy to invert while extending conditions.

When you see a petty crime, do not auto-fix it blindly. Read the evidence, then decide whether the current edit needs to preserve, encode, rename, or delete the suspicious pattern. For long-form reference, see docs/finding-types/petty.md.


Every finding has the same shape (see json-schema.md for the full type). The five fields that matter most to an agent are:

FieldWhat it tells you
severity"high" | "medium" | "low" — the headline triage signal
file + linesWhere the smell lives. lines is [start, end], inclusive, 1-based
symbolThe function or method name when applicable (e.g. for large_function findings)
evidenceConcrete facts the detector observed — quote these back when explaining changes
scores.agent_risk0–1. Higher means easier for an LLM to misread, duplicate, or break this area

severity is for triage. scores.agent_risk is the more interesting signal for an agent: it stays high even when the local severity is low, when the area is structurally confusing (multiple sources of truth, weak tests, hidden side effects, etc.).

scores.test_gap — note for agents comparing exact values. From 0.10.0, test_gap is a repo-relative quartile value (0 / 0.25 / 0.5 / 0.75 / 1.0) rather than the fixed mapping ({0, 0.5, 1.0}) used before. Agents that compared test_gap === 1 should switch to test_gap >= 0.75.

Finding.tier (new in 0.10.0) is "domain" | "nonDomain". Non-domain findings come from paths listed in config.scopeTiers.nonDomain (defaults: scripts/**, examples/**, fixtures/**, public/**, **/__tests__/**, **/*.test.{ts,tsx,js,jsx}, **/*.spec.{ts,tsx,js,jsx}). They appear in a separate “Also flagged elsewhere” footer in the human scan report and don’t compete with domain findings for the default top-N. Set scopeTiers.nonDomain: [] in crimes.config.json to opt out.

Default sort order is aggregate risk-first (rank_score, derived from severity, confidence, churn, test-gap, blast radius, and recency), then file path, then line number. You can re-sort by scores.agent_risk if your goal is “what should I read before editing”, rather than “what is most broken”.


This is the actual current output of pnpm scan:example:json against examples/messy-ts-app, with the absolute repo path sanitised:

{
"schema_version": "0.1.0",
"report_type": "scan",
"repo": {
"name": "messy-ts-app",
"root": "/path/to/crimes/examples/messy-ts-app"
},
"summary": { "total": 19, "high": 0, "medium": 13, "low": 6 },
"findings": [
// …19 findings elided. The bundled fixture suppresses the
// `large_function::src/billing.ts::generateInvoice` God Function
// in .crimes/suppressions.json as a demonstration of the v0.5.0
// workflow — see docs/suppressions.md. Run with --show-suppressed
// to re-surface it annotated; rerun without the suppressions file
// to see the original 20-finding output.
],
"suppressed_count": 1
}

Full file: docs/fixtures/messy-ts-app.json.


The brief above describes the workflow crimes is built around. Some of the commands the PRD calls out are not yet implemented, and you should not rely on them in agent instructions yet:

CommandStatus
crimes scan [path]✅ shipped
crimes scan [path] --format json✅ shipped
crimes scan --all✅ shipped
crimes scan --no-color✅ shipped
crimes scan --changed✅ shipped
crimes scan --changed --base <ref>✅ shipped
crimes scan --changed --fail-on <severity>✅ shipped (0.2.0)
crimes scan --top <n>✅ shipped (0.10.0)
crimes scan --flat✅ shipped (0.10.0)
crimes scan --no-recency✅ shipped (0.10.0)
crimes context <file>✅ shipped
crimes context <file> --format json✅ shipped
crimes hotspots [path]✅ shipped
crimes hotspots [path] --since <window>✅ shipped
crimes hotspots [path] --format json✅ shipped
crimes diff <base...head>✅ shipped (0.2.0)
crimes diff <base...head> --format json✅ shipped (0.2.0)
crimes baseline save [path]✅ shipped (0.2.0)
crimes baseline check [path]✅ shipped (0.2.0)
crimes baseline check --fail-on <severity>✅ shipped (0.2.0)
crimes baseline check --format json✅ shipped (0.2.0)
crimes verdict✅ shipped (0.2.0)
crimes verdict --base <ref>✅ shipped (0.2.0)
crimes verdict --format json✅ shipped (0.2.0)
crimes verdict --fail-on <threshold>✅ shipped (0.2.0)
IA findings: missing_agent_context, route_metadata_drift, duplicated_navigation_source, concept_alias_drift, docs_code_drift✅ shipped (0.3.0)
Finding.related_files populated on IA findings (human-rendered as “Also touches:”)✅ shipped (0.3.0)
Petty crimes: commented_out_code, logic_in_comments, name_behavior_mismatch, magic_domain_literal_scatter, weak_test_signal, option_bag_junk_drawer, return_shape_roulette, negative_flag_maze✅ shipped (0.3.0)
crimes diff --fail-on new-high | new-medium✅ shipped (0.5.0)
crimes ignore <id-or-fingerprint> --reason "…"✅ shipped (0.5.0)
crimes unignore <fingerprint>✅ shipped (0.5.0)
crimes audit-suppressions✅ shipped (0.5.0)
crimes explain <id-or-fingerprint> [--from <scan.json>]✅ shipped (0.5.0)
crimes init [--force]✅ shipped (0.5.0)
crimes init --no-detect✅ shipped (0.10.0)
--show-suppressed on scan / context / baseline check / diff / verdict✅ shipped (0.5.0)
Finding.suppressed / suppression_reason / *Report.suppressed_count✅ shipped (0.5.0)
Per-finding scores.churn / scores.test_gap / scores.blast_radius✅ shipped (0.6.0)
Dependency-graph detectors (layer_violation, circular_dependency, deep_import, high_fan_in_fan_out)✅ shipped (0.6.0)
IA completion (orphaned_destination, parallel_destination, permission_ia_drift, action_label_drift, command_drift_docs_code_drift)✅ shipped (0.6.0)
Frontend agent-risk (design_token_escape, accessible_interaction_risk, duplicate_component_shape, responsive_fragility, copy_ia_drift)✅ shipped (0.6.0)
Duplication (exact_duplicate_block, near_duplicate_block, duplicated_role_status_plan_check)✅ shipped (0.6.0)
large_function cli_command_registrar shape✅ shipped (0.6.0)
crimes hotspots <subdir> enclosing-repo lookup✅ shipped (0.6.0)
detectors.disable stderr breadcrumb✅ shipped (0.6.0)
detectors.options.<id> per-detector exemption config✅ shipped (0.8.0)
Date / time detectors (timezone_unsafe_parse, mixed_utc_local_methods, locale_drift, dst_naive_arithmetic, date_string_concat)✅ shipped (0.8.0)
Naming-tier detectors (boolean_naming_drift, singular_plural_type_mismatch)✅ shipped (0.8.0)
Hot-path / portability detectors (sync_io_in_hotpath, hardcoded_local_path, hardcoded_localhost)✅ shipped (0.8.0)
Asset detectors (oversized_raster, raster_should_be_vector, svg_with_embedded_raster) — second-pass walk over **/*.{png,jpg,jpeg,gif,webp,avif,svg}✅ shipped (0.8.0)
thresholds.assetWeight.{lowKb,mediumKb,highKb} + assets.include/exclude config✅ shipped (0.8.0)
File-grouped scan layout (crimes scan default)✅ shipped (0.10.0)
Finding.tier + scope-tier classifier✅ shipped (0.10.0)
Finding.scores.recency (0–1 decay factor)✅ shipped (0.10.0)
scores.test_gap repo-relative quartile ranking✅ shipped (0.10.0)
ContextReport.clues block✅ shipped (0.10.0)
scopeTiers.nonDomain config key✅ shipped (0.10.0)
scan.topFiles config key✅ shipped (0.10.0)
Two-prompt auto-init with agent detection✅ shipped (0.10.0)
crimes triage (interactive walk + --apply / --list / --clear / --retriage)✅ shipped (0.11.0)
Finding.effort + Finding.fix_shape (schema 0.2.0)✅ shipped (0.11.0)
previously_triaged / previous_triage / previously_baselined / previous_baseline on findings✅ shipped (0.11.0)
triage.resurfaceBase config key + automatic resurfacing on touched files✅ shipped (0.11.0)
crimes scan --show-triaged / --gate-needs-design / --gate-resurfaced✅ shipped (0.11.0)
crimes init --agents writes .claude/settings.local.json PreToolUse hook (--no-hooks opts out)✅ shipped (0.11.0)
Human-readable secondary scores in scan + context (JSON numerics unchanged)✅ shipped (0.11.0)
crimes ask / LLM-assisted modes🚧 deferred to v1+

The pre/post-edit workflow works as crimes context <file> --format json before touching a file, crimes scan --changed --format json after, and crimes verdict for the one-line “did this branch help or hurt?” summary at the end of a task. For a hard CI gate you have four equivalent options: crimes scan --changed --fail-on, crimes baseline check --fail-on, crimes diff --fail-on new-high | new-medium, and crimes verdict --fail-on — see docs/ci.md.

When a finding turns out to be deliberate (a legacy module under rewrite, a route handler the team has agreed to keep monolithic, an alias kept for backwards compatibility), the right answer is explain then ignore, not silently skipping the report:

Terminal window
crimes explain large_function::src/billing.ts::generateInvoice
# → reads the rationale; the agent (or human) decides this is acceptable
crimes ignore large_function::src/billing.ts::generateInvoice \
--reason "Legacy billing module — rewrite tracked in #1234"

crimes ignore requires a --reason and persists it to .crimes/suppressions.json, which the team commits and reviews in PRs. Agents should phrase the reason as a single specific sentence naming the constraint or tracking issue — “too noisy” or “we know about this” are not acceptable suppression reasons.

When a suppression becomes obsolete (issue resolved, code refactored, team decision reversed), remove it:

Terminal window
crimes unignore large_function::src/billing.ts::generateInvoice
# → "Removed … from .crimes/suppressions.json. Commit the change …"

To audit the existing suppressions file — sorted by age, with automatic flags for stale (>180d), short (<16 chars), or vague reasons — run:

Terminal window
crimes audit-suppressions
crimes audit-suppressions --format json

See docs/suppressions.md for the full workflow.


  • schema_version at the top of the report is the source of truth. crimes@0.11.0 bumps it from "0.1.0" to "0.2.0" to add the required effort and fix_shape fields on every Finding. The shape documented in json-schema.md is stable within a schema_version; consumers that hard-checked === "0.1.0" must accept "0.2.0" as well.
  • Optional score fields (scores.blast_radius, scores.churn, scores.test_gap) are populated by every scan from 0.6.0 onward from the import graph, 90-day git churn, and the test-file index. They remain optional in the schema so consumers can keep tolerating absence in mixed-version environments (a crimes scan from a fixture saved before 0.6.0 still parses cleanly). See scoring.md for the unified agent_risk formula.
  • scores.test_gap changed from a fixed three-point mapping to a repo-relative quartile scale in 0.10.0. Values are now 0 / 0.25 / 0.5 / 0.75 / 1.0. Agents that compared test_gap === 1 should switch to test_gap >= 0.75.
  • Finding.tier, Finding.scores.recency, and ContextReport.clues are new optional fields added in 0.10.0. They are absent on scan output from earlier versions; tolerate their absence in mixed-version environments.
  • related_files is populated by the IA detectors (since 0.3.0). Treat its absence on a structural finding as “no cross-file context for this finding”.
  • Breaking changes to the wire format will bump schema_version. Agents should refuse to consume output whose schema_version they don’t know.

Default crimes scan is advisory — it always exits 0, regardless of findings. The same goes for crimes diff, crimes context, crimes hotspots, and crimes verdict (without --fail-on).

Four commands have an opt-in gating mode:

CommandExit 1 when …
crimes scan --changed --fail-on low|medium|highA finding in the changed set meets the severity threshold.
crimes baseline check --fail-on low|medium|high (default medium)A new finding (vs the saved baseline) meets the severity threshold.
crimes diff --fail-on new-high|new-medium (0.5.0)A new finding in the diff meets the severity threshold.
crimes verdict --fail-on worse|new-high|new-mediumThe configured verdict / severity threshold is hit.

All three share the same exit-code contract:

ExitMeaning
0Command succeeded; no blocking findings under the configured --fail-on.
1Blocking findings present — the CI gate.
2Usage / environment error — bad flag, missing baseline, not a git repo, etc.

0 and 1 always emit JSON on stdout when --format json is set. 2 writes a human-readable error to stderr and emits no JSON, so consumers can distinguish “gate failed” from “command broke” without parsing.

If you want to gate on a command that doesn’t have --fail-on (e.g. plain crimes scan without --changed), gate on the JSON yourself:

Terminal window
crimes scan . --format json \
| jq -e '.summary.high == 0' >/dev/null

See docs/ci.md for the CI-side recipes built on these exit codes.