`crimes.config.json` reference
Zero-config works for most repos. Use crimes.config.json only when the
defaults are wrong for your repo — tuning thresholds, disabling
detectors that don’t apply, seeding product-specific concept aliases.
The config lives at the repo root as crimes.config.json. The
.crimes/ directory next to it is a tooling output directory
(baseline, suppressions, cache); the config is hand-edited.
Bootstrap
Section titled “Bootstrap”npx crimes initWrites a starter crimes.config.json with sensible defaults and an
inline $schema URL for IDE validation. Refuses to overwrite an
existing file unless you pass --force.
To also make crimes discoverable to future Claude Code and Codex
sessions in the repo:
npx crimes init --agentsThat writes .claude/skills/crimes/SKILL.md and
.agents/skills/crimes/SKILL.md alongside the config.
{ "$schema": "https://crimes.sh/schema/0.1.0/config.json",
"include": ["**/*.{ts,tsx,js,jsx,mjs,cjs}"], "exclude": [ "**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/out/**", "**/coverage/**", "**/*.min.js", "**/*.generated.*", "**/.crimes/**" ],
"thresholds": { "largeFileLines": 300, "largeFunctionLines": 60, "todoDensityPerKLoc": 10, "largeFunction": { "domain": 60, "route_handler": 100, "react_component": 200, "page_export": 200, "test_callback": 200, "cli_command_registrar": 200, "unknown": 80 }, "largeFile": { "domain": 300, "test_file": 1500 } },
"detectors": { "enable": [], "disable": [] },
"scopeTiers": { "nonDomain": [ "scripts/**", "examples/**", "fixtures/**", "public/**", "**/__tests__/**", "**/*.test.{ts,tsx,js,jsx}", "**/*.spec.{ts,tsx,js,jsx}" ] },
"scan": { "topFiles": 5 },
"triage": { "resurfaceBase": "main" },
"ia": { "aliasGroups": [] },
"suppressions": { "path": ".crimes/suppressions.json" },
"architecture": { "layers": [], "rules": [] }}Every key is optional. Missing keys take the defaults documented in
packages/core/src/config.ts.
Field reference
Section titled “Field reference”$schema
Section titled “$schema”Optional URL pointing at the JSON schema. Parsed but not consumed by the CLI — there only for IDE validation.
include / exclude
Section titled “include / exclude”Glob patterns the file walker honours. The included paths must match
and the excluded patterns must not match. Identical defaults to
crimes init.
thresholds.largeFileLines / largeFunctionLines / todoDensityPerKLoc
Section titled “thresholds.largeFileLines / largeFunctionLines / todoDensityPerKLoc”The original three knobs. largeFunctionLines is the domain
function threshold; it stays the back-compat alias for the per-shape
override below.
thresholds.largeFunction.<shape>
Section titled “thresholds.largeFunction.<shape>”Per-shape large_function overrides. Any subset is fine — unset shapes
use the built-in defaults:
| Shape | Default threshold |
|---|---|
domain | 60 |
route_handler | 100 |
react_component | 200 |
page_export | 200 |
test_callback | 200 |
cli_command_registrar | 200 |
unknown | 80 |
The cli_command_registrar shape (new in 0.6.0) covers Commander-
style register*Command(program) wrapper functions and their
anonymous .action(...) callbacks. The chain is declarative DSL,
not branching logic, so the threshold is generous and severity caps
at low / medium.
thresholds.largeFunction.domain wins over the legacy
thresholds.largeFunctionLines when both are set.
thresholds.largeFile.<shape>
Section titled “thresholds.largeFile.<shape>”Per-shape large_file overrides (new in 0.6.0). Any subset is fine
— unset shapes use the built-in defaults:
| Shape | Default threshold |
|---|---|
domain | 300 |
test_file | 1500 |
The test_file shape matches **/*.{test,spec}.[jt]sx? and files
under __tests__/. Test suites legitimately grow large with many
small it() blocks, so the budget is much higher and severity caps
at low / medium.
thresholds.largeFile.domain wins over the legacy
thresholds.largeFileLines when both are set.
thresholds.assetWeight
Section titled “thresholds.assetWeight”Severity thresholds for oversized_raster (new in 0.8.0). Sizes
are in KB (1 KB = 1024 bytes); any subset is fine — unset levels
use the built-in defaults:
| Knob | Default |
|---|---|
lowKb | 200 |
mediumKb | 500 |
highKb | 1000 |
The severity rule: bytes below lowKb produce no finding;
[lowKb, mediumKb) is low; [mediumKb, highKb) is medium;
≥ highKb is high. Defaults mirror Core Web Vitals “good /
needs improvement / poor” guidance for content images.
assets.include / assets.exclude
Section titled “assets.include / assets.exclude”Asset-file discovery overrides for the second-pass detectors
(oversized_raster, raster_should_be_vector,
svg_with_embedded_raster). Independent of the top-level
include / exclude so users can tune asset scope without
touching source scope. Defaults:
{ "assets": { "include": ["**/*.{png,jpg,jpeg,gif,webp,avif,svg}"], "exclude": [ "**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/out/**", "**/coverage/**", "**/.crimes/**", "**/public/vendor/**", "**/__snapshots__/**", "**/fixtures/**", "**/*.test.{png,jpg,jpeg,gif,webp,avif,svg}" ] }}Setting assets.include to an empty list disables the asset pass
entirely.
detectors.enable / detectors.disable
Section titled “detectors.enable / detectors.disable”enableis an allowlist. Empty or omitted means “run all built-ins”. When non-empty, only the listed ids run.disableis a blocklist that runs afterenable.- An unknown detector id in either list raises a CLI error (exit
2) — typos should not silently no-op. See the table injson-schema.mdfor the full list of ids.
Anti-pattern: disabling a detector is a blunt tool. Prefer
suppressing specific findings with crimes ignore plus a reason.
Reserve disable for detectors that fundamentally don’t fit your
repo (todo_density on a research codebase where TODO is a tracking
convention, not debt).
detectors.options
Section titled “detectors.options”Per-detector exemption values. Sits between detectors.disable
(kills the detector everywhere) and crimes ignore (kills one
specific finding) — detectors.options.<id> lets you say “this
value is fine for this detector across the whole codebase, but
keep firing on others.”
{ "detectors": { "options": { "boolean_naming_drift": { "allowedNames": ["loading", "ready"] }, "magic_domain_literal_scatter": { "allowedLiterals": ["draft", "published"] } } }}Each detector that accepts options declares its own schema; the
config loader validates supplied options against the schema at
load time. Three failure modes — all CLI exit 2
(ConfigParseError):
detectors.options.<id>: unknown detector id — the id doesn’t match any built-in.detectors.options.<id>: this detector accepts no options — the id is real but the detector has not registered any options schema.detectors.options.<id>: … — the value’s shape doesn’t match the detector’s declared options.
The keys each detector understands are documented alongside that
detector under finding-types/. Detectors
that don’t appear in those docs accept no options.
scopeTiers.nonDomain (since 0.10.0)
Section titled “scopeTiers.nonDomain (since 0.10.0)”Glob patterns whose findings are classified as tier: "nonDomain".
Non-domain findings appear in a separate “Also flagged elsewhere”
footer in the default crimes scan human report and don’t compete
with domain findings for the default top-N file slots.
{ "scopeTiers": { "nonDomain": [ "scripts/**", "examples/**", "fixtures/**", "public/**", "**/__tests__/**", "**/*.test.{ts,tsx,js,jsx}", "**/*.spec.{ts,tsx,js,jsx}" ] }}If scopeTiers is omitted from the config, the seven-pattern default
above is applied at scan time. Set "nonDomain": [] to opt out
entirely (every finding becomes "domain").
scan.topFiles (since 0.10.0)
Section titled “scan.topFiles (since 0.10.0)”Default number of files shown in the file-grouped crimes scan human
output. Override per-invocation with --top N; pass --all to see
every finding (both tiers, no cap); pass --flat to revert to the
legacy severity-grouped layout.
{ "scan": { "topFiles": 10 }}Defaults to 5. JSON output is unaffected — the topFiles knob only
shapes the human renderer.
triage.resurfaceBase (since 0.11.0)
Section titled “triage.resurfaceBase (since 0.11.0)”Git ref used to detect “touched files” for the resurfacing pipeline.
On every crimes scan invocation, files in the diff against
<resurfaceBase>...HEAD (plus working-tree changes) are checked
against .crimes/triage.json and .crimes/baseline.json; any
silenced or baselined finding whose file is in that set resurfaces
with previously_triaged / previous_triage (or
previously_baselined / previous_baseline) annotations.
| Key | Type | Default | Description |
|---|---|---|---|
triage.resurfaceBase | string | "main" | Git ref used to detect “touched files” for resurfacing. Empty string disables resurfacing entirely. |
{ "triage": { "resurfaceBase": "develop" }}Resurfacing is skipped silently when:
triage.resurfaceBaseis"".- The directory is not a git repository.
HEADresolves to the same ref as<resurfaceBase>— you’re on the base, there’s no diff to compute.
Interaction with scopeTiers.nonDomain. Resurfacing crosses
tiers — a triaged finding in a non-domain file (e.g. under
scripts/** or **/__tests__/**) still resurfaces when that file
is in the branch diff. The non-domain footer is a display tier, not
a “we don’t care about it” tier; once you’ve explicitly triaged the
finding the resurface contract honours that decision regardless of
where the file lives.
The default crimes triage interactive walk visits domain-tier
findings only. Pass crimes triage --all to include non-domain
findings in the walk.
ia.aliasGroups
Section titled “ia.aliasGroups”Seed entries for concept_alias_drift. Each group is { id, aliases[], preferred? } with lowercase, singular tokens. Always
additive to the built-in DEFAULT_ALIAS_GROUPS.
{ "ia": { "aliasGroups": [ { "id": "dataset", "aliases": ["dataset", "corpus", "collection"] } ] }}suppressions.path
Section titled “suppressions.path”Override the on-disk suppressions file path. Defaults to
.crimes/suppressions.json. Relative paths resolve against the repo
root; absolute paths win unchanged. See
suppressions.md.
architecture (consumed by layer_violation since 0.6.0)
Section titled “architecture (consumed by layer_violation since 0.6.0)”Defines named layers by file glob and explicit
from → cannotImport rules. The layer_violation detector consumes
both fields and emits one finding per imported file that crosses a
forbidden boundary.
{ "architecture": { "layers": [ { "name": "ui", "pattern": "src/components/**" }, { "name": "domain", "pattern": "src/domain/**" }, { "name": "db", "pattern": "src/db/**" } ], "rules": [ { "from": "ui", "cannotImport": ["db", "domain"] }, { "from": "domain", "cannotImport": ["ui"] } ] }}pattern is a glob matched against repo-relative POSIX paths.
from and cannotImport[] reference layers by name. Layers that
don’t appear in any rule are still useful as documentation; the
detector only emits findings when a cannotImport is violated.
See docs/finding-types/dependency.md
for the full detector contract and PRD.md §18 for the design intent.
Worked examples
Section titled “Worked examples”Add a product-specific alias group
Section titled “Add a product-specific alias group”{ "ia": { "aliasGroups": [ { "id": "tenant", "aliases": ["tenant", "company", "org", "organization"] } ] }}Tune large_function for a route-heavy app
Section titled “Tune large_function for a route-heavy app”{ "thresholds": { "largeFunction": { "route_handler": 150 } }}Disable a detector that doesn’t apply
Section titled “Disable a detector that doesn’t apply”{ "detectors": { "disable": ["todo_density"] }}Move the suppressions file out of .crimes/
Section titled “Move the suppressions file out of .crimes/”{ "suppressions": { "path": "config/crimes-suppressions.json" }}Validation errors
Section titled “Validation errors”The CLI validates the file with zod. A malformed value prints the
exact key path that failed and exits 2:
crimes: crimes.config.json at .../crimes.config.json is invalid:thresholds.largeFileLines: Expected number, received stringUnknown top-level keys are preserved silently — crimes may extend
the schema in future releases without breaking older config files.