Skip to content

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 by heal init and heal calibrate. Don’t hand-edit it; put floor_critical / floor_ok overrides in config.toml instead 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.

For most projects, the defaults from heal init work. Edit only to override what doesn’t fit:

[project]
response_language = "Japanese"
[git]
since_days = 90
exclude_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 = 2
MetricDefault
LOCalways enabled (no toggle)
Churnenabled
Complexity (CCN)enabled
Cognitiveenabled
Duplicationenabled
Change Couplingenabled (incl. symmetric)
Hotspotenabled
LCOMenabled

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]
response_language = "Japanese"
  • response_language — language hint passed to Claude skills. Any value Claude understands works: "Japanese", "日本語", "français", "plain English". Optional.

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 under tests/).
  • exclude_paths (optional) — gitignore-syntax patterns evaluated relative to the workspace root, layered on top of git.exclude_paths.
[[project.workspaces]]
path = "packages/api"
language = "typescript"
exclude_paths = ["vendor/", "src/generated/**"]

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 = 18

Available 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.

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).
heal status --workspace packages/api # findings under packages/api only
heal status --json --workspace packages/web # JSON, scoped

Used by every metric that walks git history (churn, change coupling, hotspot).

[git]
since_days = 90
exclude_paths = ["dist/"]
  • since_days (default 90) — 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]
top_n = 5
  • top_n (default 5) — 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 as Ok.
[metrics.loc]
inherit_git_excludes = true
exclude_paths = []
  • inherit_git_excludes (default true) — combine with git.exclude_paths.
  • exclude_paths — LOC-only gitignore-syntax patterns.
[metrics.churn]
top_n = 10

Window length comes from git.since_days. Disable Churn entirely via [metrics] disabled = ["churn", ...] rather than a per-section flag.

[metrics.ccn]
floor_critical = 25 # McCabe "untestable in practice"
floor_ok = 11 # McCabe "simple, low risk"
[metrics.cognitive]
floor_critical = 50 # SonarQube Critical baseline
floor_ok = 8 # half of Sonar's "review" threshold

Defaults 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]
min_tokens = 50
floor_critical = 30 # 30% duplicate is a structural problem
  • min_tokens (default 50) — minimum window for a duplicate block over code. Lower values surface shorter blocks.
  • docs_min_tokens (default 100) — minimum window for the Markdown / RST duplication pass. Only used when [features.docs] is on. See Docs › Metrics.
[metrics.change_coupling]
min_coupling = 3
symmetric_threshold = 0.5
  • min_coupling (default 3) — pairs that co-changed less often than this are dropped before ranking.
  • symmetric_threshold (default 0.5) — both P(B|A) and P(A|B) must reach this for a pair to classify as Symmetric.
[metrics.hotspot]
weight_churn = 1.0
weight_complexity = 1.0
  • The composed score is (weight_complexity × ccn_sum) × (weight_churn × commits). Setting either weight to 0.0 disables 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]
min_cluster_count = 2
  • min_cluster_count (default 2) — classes with fewer clusters than this are dropped before Severity classification. 2 is the natural baseline (the class is mechanically separable).

Tunes heal diff’s worktree-backed mode.

[diff]
max_loc_threshold = 200_000
  • max_loc_threshold (default 200_000) — total LOC ceiling for materialising a temporary git worktree to 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.

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 zero
should = ["critical", "high:hotspot"] # T1 — drain when convenient

DSL grammar:

  • <severity> — match the severity, any hotspot.
  • <severity>:hotspot — match the severity AND hotspot = true.

Severity tokens are lowercase: critical, high, medium, ok. Unknown tokens fail at config-load time.

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 = 142
calibrated_at_sha = "a0a6d1a7f3…"
strategy = "percentile"
[calibration.ccn]
p50 = 4.2
p75 = 8.1
p90 = 14.3
p95 = 21.7
floor_critical = 25.0
floor_ok = 11.0
[calibration.hotspot]
p50 = 5.0
p75 = 18.0
p90 = 67.0 # Hotspot 🔥 flag boundary (top 10%, fixed)
p95 = 145.0

heal 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.

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 here

Errors 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.