Code · Configuration
heal init writes two TOML files under .heal/:
config.toml— every observer toggle and tunable. Edit this freely.calibration.toml— codebase-relative percentile thresholds. Auto-generated byheal initandheal calibrate. Don’t hand-edit it; putfloor_critical/floor_okoverrides inconfig.tomlinstead so they survive recalibration.
Both files are per-repository; there is no global config. heal re-reads them on every invocation — no daemon to restart.
This page covers the always-on Code family. For the opt-in families see Test › Configuration and Docs › Configuration.
A typical config
Section titled “A typical config”For most projects, the defaults from heal init work. Edit only
to override what doesn’t fit:
[project]response_language = "Japanese"
[git]since_days = 90exclude_paths = ["dist/", "vendor/", "node_modules/", ".cache/"]
[metrics]top_n = 5
[metrics.hotspot]weight_complexity = 1.5 # bias toward complexity over churn
[metrics.lcom]min_cluster_count = 2Defaults at a glance
Section titled “Defaults at a glance”| Metric | Default |
|---|---|
| LOC | always enabled (no toggle) |
| Churn | enabled |
| Complexity (CCN) | enabled |
| Cognitive | enabled |
| Duplication | enabled |
| Change Coupling | enabled (incl. symmetric) |
| Hotspot | enabled |
| LCOM | enabled |
Disable a metric by adding its name to the top-level
[metrics] disabled = [...] list. A disabled metric is skipped
entirely — its findings never appear in heal status. Names are
the snake_case form (lcom, change_coupling, …); loc cannot
be disabled (every other observer depends on it).
[metrics]disabled = ["lcom"][project]
Section titled “[project]”[project]response_language = "Japanese"response_language— language hint passed to Claude skills. Any value Claude understands works:"Japanese","日本語","français","plain English". Optional.
[[project.workspaces]] — monorepos
Section titled “[[project.workspaces]] — monorepos”For a single-package repo, skip this section. In a monorepo, you usually want each workspace calibrated against its own distribution — a 5kloc CLI shouldn’t share a complexity ladder with the 50kloc API next to it. Declare each workspace once:
[[project.workspaces]]path = "packages/web"language = "typescript"
[[project.workspaces]]path = "packages/api"language = "typescript"
[[project.workspaces]]path = "services/worker"language = "rust"Each entry takes:
path— repo-root-relative directory (slash-separated, no leading/). Workspaces cannot nest.language(optional) — override the auto-detected primary language. Useful when LOC’s heuristic picks the wrong one (e.g. a Rust workspace with a heavy JavaScript fixture set undertests/).exclude_paths(optional) — gitignore-syntax patterns evaluated relative to the workspace root, layered on top ofgit.exclude_paths.
[[project.workspaces]]path = "packages/api"language = "typescript"exclude_paths = ["vendor/", "src/generated/**"]Per-workspace floor overrides
Section titled “Per-workspace floor overrides”Tighten or relax the absolute floors for one workspace without touching the others:
[[project.workspaces]]path = "packages/legacy"language = "typescript"
# Known-high-complexity legacy area; relax the graduation gate# while it's being migrated out.[project.workspaces.metrics.ccn]floor_ok = 18Available per-metric overrides: floor_critical and floor_ok
(ccn / cognitive / duplication / change_coupling / lcom).
Workspace overrides win over global [metrics.<m>] overrides.
The percentile breaks are computed per-workspace automatically —
just declaring [[project.workspaces]] is enough.
Cross-workspace coupling
Section titled “Cross-workspace coupling”When a co-changing pair spans two workspaces (a “module-boundary
leak”), heal retags it as change_coupling.cross_workspace:
[metrics.change_coupling]cross_workspace = "surface" # or "hide"surface(default) — cross-workspace pairs go to Advisory; they surface as a signal but don’t enter the drain queue.hide— drop them entirely. Useful when the coupling is intentional (shared schema, deliberately co-evolving APIs).
Filtering output by workspace
Section titled “Filtering output by workspace”heal status --workspace packages/api # findings under packages/api onlyheal status --json --workspace packages/web # JSON, scopedUsed by every metric that walks git history (churn, change coupling, hotspot).
[git]since_days = 90exclude_paths = ["dist/"]since_days(default90) — lookback window for churn / coupling.exclude_paths— gitignore-syntax patterns. The full DSL works: globs (*,**,?,[abc]), directory-only (foo/), root anchoring (/foo), negation (!keep), comments (#).
LOC inherits this list by default; other observers always respect it.
[metrics]
Section titled “[metrics]”[metrics]top_n = 5top_n(default5) — default size for every “worst-N” listing. Each observer can override.
Every per-observer subsection below shares:
enabled— master toggle (LOC has none).top_n(optional) — override the global default.floor_critical(optional, where applicable) — absolute Severity floor that beats percentile breaks.floor_ok(optional, proxy metrics only) — absolute graduation gate. Anything strictly below this classifies asOk.
[metrics.loc]
Section titled “[metrics.loc]”[metrics.loc]inherit_git_excludes = trueexclude_paths = []inherit_git_excludes(defaulttrue) — combine withgit.exclude_paths.exclude_paths— LOC-only gitignore-syntax patterns.
[metrics.churn]
Section titled “[metrics.churn]”[metrics.churn]top_n = 10Window length comes from git.since_days. Disable Churn entirely via
[metrics] disabled = ["churn", ...] rather than a per-section flag.
[metrics.ccn] and [metrics.cognitive]
Section titled “[metrics.ccn] and [metrics.cognitive]”[metrics.ccn]floor_critical = 25 # McCabe "untestable in practice"floor_ok = 11 # McCabe "simple, low risk"
[metrics.cognitive]floor_critical = 50 # SonarQube Critical baselinefloor_ok = 8 # half of Sonar's "review" thresholdDefaults are literature-anchored. Override only when your domain
warrants stricter or laxer thresholds. When you do, heal status
prints the override on the header line so policy changes are
auditable in CI logs.
[metrics.duplication]
Section titled “[metrics.duplication]”[metrics.duplication]min_tokens = 50floor_critical = 30 # 30% duplicate is a structural problemmin_tokens(default50) — minimum window for a duplicate block over code. Lower values surface shorter blocks.docs_min_tokens(default100) — minimum window for the Markdown / RST duplication pass. Only used when[features.docs]is on. See Docs › Metrics.
[metrics.change_coupling]
Section titled “[metrics.change_coupling]”[metrics.change_coupling]min_coupling = 3symmetric_threshold = 0.5min_coupling(default3) — pairs that co-changed less often than this are dropped before ranking.symmetric_threshold(default0.5) — bothP(B|A)andP(A|B)must reach this for a pair to classify asSymmetric.
[metrics.hotspot]
Section titled “[metrics.hotspot]”[metrics.hotspot]weight_churn = 1.0weight_complexity = 1.0- The composed score is
(weight_complexity × ccn_sum) × (weight_churn × commits). Setting either weight to0.0disables that side of the composition.
Hotspot doesn’t have a floor_critical; it’s a flag (top 10% of
the score distribution), not a Severity tier.
[metrics.lcom]
Section titled “[metrics.lcom]”[metrics.lcom]min_cluster_count = 2min_cluster_count(default2) — classes with fewer clusters than this are dropped before Severity classification.2is the natural baseline (the class is mechanically separable).
[diff]
Section titled “[diff]”Tunes heal diff’s worktree-backed mode.
[diff]max_loc_threshold = 200_000max_loc_threshold(default200_000) — total LOC ceiling for materialising a temporarygit worktreeto scan a different ref. Above this,heal diff <ref>exits with code 2 and prints a manual two-branch recipe instead of cloning a worktree.
[policy.drain]
Section titled “[policy.drain]”The drain policy decides which (Severity, hotspot) combinations
/heal-code-patch must drain (T0) vs may drain when bandwidth
permits (T1). Anything outside both lists falls to Advisory and is
shown only with --all.
[policy.drain]must = ["critical:hotspot"] # T0 — drain to zeroshould = ["critical", "high:hotspot"] # T1 — drain when convenientDSL grammar:
<severity>— match the severity, any hotspot.<severity>:hotspot— match the severity ANDhotspot = true.
Severity tokens are lowercase: critical, high, medium, ok.
Unknown tokens fail at config-load time.
.heal/calibration.toml
Section titled “.heal/calibration.toml”Generated by heal init and refreshed by heal calibrate --force.
Don’t hand-edit — floor_critical and floor_ok belong in
config.toml so they survive recalibration.
[meta]created_at = "2026-04-30T09:00:00Z"codebase_files = 142calibrated_at_sha = "a0a6d1a7f3…"strategy = "percentile"
[calibration.ccn]p50 = 4.2p75 = 8.1p90 = 14.3p95 = 21.7floor_critical = 25.0floor_ok = 11.0
[calibration.hotspot]p50 = 5.0p75 = 18.0p90 = 67.0 # Hotspot 🔥 flag boundary (top 10%, fixed)p95 = 145.0heal calibrate (no flags) only creates the file when missing — if
it already exists, the command reports its presence without
rewriting anything. Pass --force to actually rescan. The
/heal-setup skill watches for drift and recommends
heal calibrate --force when the codebase has moved enough.
Strict by design
Section titled “Strict by design”Every section rejects unknown keys. A misspelled field produces a parse error at startup rather than being silently dropped:
[metrics]typo_n = 5 # ✘ unknown field — heal errors hereErrors include the file path and line number — silent drops are a common path for config mistakes to reach production, and we’d rather you find out immediately.