Suppressions
.crimes/suppressions.json carries per-finding exceptions the team
has deliberately decided to live with. Suppressed findings are
filtered out of every report by default; the gate (--fail-on) never
trips on them. The file is intended to be committed and reviewed
in PRs: every entry requires a reason.
When to use a suppression vs the alternatives
Section titled “When to use a suppression vs the alternatives”| Situation | Right answer |
|---|---|
| A specific finding is acceptable in this one place. | crimes ignore <fingerprint> --reason "…" |
| A particular value is fine for one detector across the whole repo. | detectors.options.<id> in configuration.md. |
You’re migrating to crimes and don’t want to fix everything first. | crimes baseline save: see ci.md. |
| A detector fundamentally doesn’t fit the repo. | detectors.disable in configuration.md. |
| A threshold is wrong for the repo. | thresholds.* in configuration.md. |
| You want to silence the entire codebase. | Don’t. Choose one of the above. |
Suppress one finding
Section titled “Suppress one finding”crimes explain 'large_function/too_long::src/billing.ts::generateInvoice'# → reads the rationale, decides this is acceptable
crimes ignore 'large_function/too_long::src/billing.ts::generateInvoice' \ --reason "Legacy billing module — rewrite tracked in #1234."Copy the emitted fingerprint from crimes scan --format json. It is opaque;
claims and discriminators can distinguish several findings on the same file.
Do not construct a fingerprint from a detector name or save a positional ID.
The CLI accepts IDs by performing a fresh scan, so an ID from a retained report
can select a different observation if the scan changed. Prefer fingerprints.
When old pins stop matching, preview a migration and review the candidates before applying it. A renamed, excluded or unparsed subject does not establish that the problem is fixed. Migration preserves the recorded reason and expiry; re-recording is a new decision.
--reason is required and non-empty. The CLI refuses to write
without one.
Other flags
Section titled “Other flags”--file <path>: override.crimes/suppressions.json.--dry-run: print the entry that would be written and exit.--no-verify: skip the fresh scan that confirms the fingerprint matches a real finding. Useful for pre-emptively suppressing a finding the detector hasn’t seen yet (rare).
File shape
Section titled “File shape”{ "schema_version": "0.8.0", "report_type": "suppressions", "created_at": "2026-05-17T11:30:00.000Z", "updated_at": "2026-05-17T11:30:00.000Z", "crimes_version": "0.28.2", "suppressions": [ { "fingerprint": "large_function/too_long::src/billing.ts::generateInvoice", "type": "large_function", "claim": "too_long", "file": "src/billing.ts", "symbol": "generateInvoice", "reason": "Legacy billing module — rewrite tracked in #1234.", "created_at": "2026-05-17T11:30:00.000Z", "created_by": "andrew@example.com" } ]}See json-schema.md
for the field-by-field reference.
created_by is filled in from git config user.email when available;
omit it if your repo doesn’t carry one. The denormalised type /
claim / file / symbol fields are redundant for matching (only
fingerprint drives it) but help human review: a
reviewer scanning git diff .crimes/suppressions.json can read the
entry without parsing the fingerprint.
Suppressing one claim of a multi-claim detector (0.8.0)
Section titled “Suppressing one claim of a multi-claim detector (0.8.0)”Eleven detectors make more than one claim, and an entry for one of
them carries a claim field naming which statement was silenced:
{ "fingerprint": "weak_test_signal/no_assertions::test/checkout.test.ts::::renders the plan", "type": "weak_test_signal", "claim": "no_assertions", "file": "test/checkout.test.ts", "reason": "asserts through a same-file helper the detector cannot follow", "created_at": "2026-08-26T09:00:00.000Z"}This is the field that makes the entry reviewable. type: "weak_test_signal" on its own reads as a judgement on the detector,
and an entry that silenced something its author never looked at would
have been indistinguishable from one that did not.
The claim is part of the fingerprint, so a suppression cannot leak
across claims: if the test later grows a toBeTruthy() and the finding
becomes weak_assertion_matchers, this entry stops matching and the new
statement surfaces for a fresh judgement. That is deliberate: the
reason recorded here is an answer to a question that is no longer being
asked.
Pins written before 0.8.0 against one of those eleven types no longer match; preview a reviewed migration before reconsidering the decision. See the migration note.
Feedback-sourced suppressions (0.7.0)
Section titled “Feedback-sourced suppressions (0.7.0)”crimes feedback ... --verdict fp writes the same suppression file,
but with two extra fields:
{ "fingerprint": "direct_date::src/suppressions.test.ts::", "type": "direct_date", "file": "src/suppressions.test.ts", "reason": "Test-file injection — intentional", "created_at": "2026-05-20T12:00:00.000Z", "source": "feedback", "crimes_version_pinned": "0.7"}source: "manual"(the default when absent): the long-standingcrimes ignorepath. These stay silent while the fingerprint still matches, until removed.source: "feedback": managed bycrimes feedback. These auto-resurface when the crimes minor moves pastcrimes_version_pinned. The next scan on a newer minor keeps the finding infindings[]taggedpreviously_suppressed: true, the human reporter prints a ”⚠ Previously marked fp in 0.7” hint per finding, and a one-line stderr notice tells you to runcrimes feedback recheck.
See feedback.md
for how feedback suppressions are reviewed across releases.
Removing a suppression
Section titled “Removing a suppression”crimes unignore 'large_function/too_long::src/billing.ts::generateInvoice'# → "Removed … from .crimes/suppressions.json. Commit the change …"crimes unignore is symmetric to crimes ignore:
- Takes a stable fingerprint (no id support: once suppressed, there is no per-scan id to look up).
--dry-runpreviews without writing.--file <path>honours the same override ascrimes ignore.- Exits
2on an unknown fingerprint, with a pointer atcrimes audit-suppressions.
The file is never deleted: an empty suppressions: [] array
stays so reviewers can see the file exists and has been intentionally
cleared. Delete it by hand if you truly want it gone.
Auditing suppressions
Section titled “Auditing suppressions”crimes audit-suppressionscrimes audit-suppressions --format jsonLists every entry sorted oldest first, with age_days and a per-entry
concerns array. Entries are flagged when:
stale: older than 180 days.short_reason:reason.trim().length < 16.vague_reason: the reason reads as a deferral keyword (tmp,todo,wip,fixme,noisy,legacy,later,skip,ignore,too noisy,we know …).
The human report groups entries into “Flagged” and “Active”. The JSON
output carries the same data under report_type: "audit_suppressions": agents can re-sort or filter without
re-running heuristics.
Run it as part of a quarterly suppression review, or wire it into a nightly CI job that watches the count and reasons.
Reviewing suppressions
Section titled “Reviewing suppressions”The file is intended to be committed. Reviewers should:
- Read the reason. “TODO” or “too noisy” usually means the
suppression is wrong: either fix the code or tune the detector.
crimes audit-suppressionssurfaces these automatically. - Verify the fingerprint maps to a real, ongoing exception. The
denormalised
file/symbolare there for this: you should recognise what is being suppressed without grepping the codebase. - Check the count and the ages. A growing
.crimes/suppressions.jsonis a smell.crimes audit-suppressionsorgit log -p .crimes/suppressions.jsonshows the trend.
crimes scan prints N findings suppressed; run with --show-suppressed to see. when ≥1 entry matched. Use
--show-suppressed to re-surface them annotated.
Suppressions and CI gates
Section titled “Suppressions and CI gates”Suppressions are applied before every --fail-on evaluation. A
suppressed finding never trips:
crimes scan --changed --fail-on <severity>crimes baseline check --fail-on <severity>crimes diff --fail-on new-high | new-mediumcrimes verdict --fail-on worse | new-high | new-medium
The gate semantics are independent of --show-suppressed: an entry
that surfaces in the output as annotated is still excluded from the
threshold check.
Suppressions vs baselines
Section titled “Suppressions vs baselines”.crimes/baseline.json | .crimes/suppressions.json |
|---|---|
| Repo-wide snapshot of pre-existing findings. | Per-finding deliberate exception with a reason. |
| Forward-only: new findings are blocked. | Manual exceptions persist; feedback exceptions resurface on later minors. |
Written by crimes baseline save. | Written by crimes ignore; removed by crimes unignore; reviewed by crimes audit-suppressions. |
Read by crimes baseline check. | Read by every report-producing command. |
Use when adopting crimes for the first time. | Use when one specific finding is acceptable. |
Most teams want both: baseline to ignore legacy debt, suppressions
to document the specific findings the team has triaged.
Anti-patterns
Section titled “Anti-patterns”- “Too noisy” as the reason. If your reason is “too noisy”, the suppression is probably wrong. Tune the detector via config (per-shape thresholds, disable on a research repo) or fix the code.
- Whole-codebase suppressions. There is no glob support on purpose: reviewing the file only works when every entry maps to one specific finding.
- Stale suppressions. Renaming a file can change its fingerprint. Review unmatched-pin warnings and migration candidates before deleting or replacing a decision.