--- title: Thresholds and baselines description: How ClawMetry decides what unusual means — four layers from module default to learned cohort baseline, the clamping band, and per-runtime environment overrides. keywords: agent detection thresholds, learned baseline anomaly, per-runtime tuning, agent behaviour baseline eyebrow: Guard & governance --- # Thresholds and baselines A threshold that is wrong is worse than no threshold. Too tight and every session is an incident; too loose and the one that mattered is not. ClawMetry resolves thresholds from four layers, each overriding the last. ## The four layers ### 1. Module default A shipped constant. `no_progress` at 20 tool calls, `stuck_loop` at 3 identical calls, blast radius at 25 files. These are the fallbacks, and every incident produced by one carries `threshold_source: "module_default"`. ### 2. The runtime profile A **checkable fact about the adapter**, not an invented number. The profile supplies each runtime's write-tool vocabulary, because "did this agent change a file" is answered differently per runtime: | Runtime | Write vocabulary | |---|---| | Claude Code | Covered by the module defaults | | Codex | `apply_patch` plus shell heredocs, caught by the shell-mutation rule | | Qwen Code, Gemini CLI, Antigravity | `replace`, plus `write_file` in defaults | | Goose | Namespaced (`developer__text_editor`); the default `edit` substring matches | | opencode | write / edit / patch, all in defaults | | PicoClaw, NanoClaw | Shell-first: every change happens inside `shell`/`exec` | | OpenWorker | `replace_in_file`, `apply_unified_diff` — declared in its own risk module | Without a profile, a runtime that edits exclusively through a tool name nothing matches looks like it made no progress at all. That is the bug this layer exists to prevent. ### 3. The learned baseline The cohort's own history, from the Guard statistics tables. Tool calls per session drives `no_progress`; distinct files mutated drives blast radius; distinct egress hosts drives `network_egress`. The learned value is `mean + σ × stddev`: | Variable | Default | Meaning | |---|---|---| | `CLAWMETRY_BASELINE_MIN_SESSIONS` | `20` | Sessions needed before a baseline is used at all | | `CLAWMETRY_BASELINE_SIGMA` | `2.0` | How many standard deviations above the mean is "unusual" | | `CLAWMETRY_BASELINE_FLOOR` | `0.5` | Lower clamp, as a ratio of the static default | | `CLAWMETRY_BASELINE_CEIL` | `5.0` | Upper clamp, as a ratio of the static default | ### 4. A per-runtime environment override The base variable with `__` appended: ```bash CLAWMETRY_NOPROG_TOOLS__CODEX=40 # tune codex alone CLAWMETRY_BLAST_FILES__CLAUDE_CODE=60 CLAWMETRY_REPEAT_FAIL_M__N8N=5 ``` The bare variable sets it for everything; the suffixed form scopes it. This is the layer to reach for when you know something about your workload that the baseline cannot learn. ## Why learning is clamped Two failure modes, both real, both prevented by the floor and ceiling: **A cohort where everything loops** would teach Guard that looping is normal and it would go blind. The floor stops the learned threshold falling below half the shipped default. **A cohort of three sessions** has a meaningless mean and a meaningless standard deviation, and would make Guard scream at every fourth session. The minimum session count stops a baseline being used at all until there is enough of it, and the ceiling stops one outlier session dragging the threshold up fivefold. ## Reading the current baselines ```bash curl -s localhost:8900/api/guard/baselines | jq ``` Each row shows the cohort, the sample size, the computed mean and standard deviation, the resulting threshold, and whether it was clamped. A clamped threshold is worth looking at — it means your fleet is far enough from the shipped assumption that one of the guards fired. ## `threshold_source` Every incident carries which layer produced its threshold: | Value | Means | |---|---| | `module_default` | A shipped constant. No measurement behind it. | | `runtime_profile` | A fact about the adapter's tool vocabulary | | `learned_baseline` | Measured from your own cohort | | `env_override` | You set it | This field exists so a reader can tell "your agents normally touch 8 files and this one touched 60" from "someone picked 25 once". Those deserve different amounts of trust, and hiding the difference would be dishonest about what the number means. ## Tuning in practice Start by not tuning. Watch the incidents for a week and read `threshold_source` on each one. - **Too many incidents on one runtime** → raise that runtime's threshold with the `__` override rather than the global one. - **Nothing ever fires** → check the baseline sample size; you may not have enough sessions yet for the learned layer, in which case you are on shipped defaults. - **A threshold shows as clamped** → your fleet is genuinely unusual for that signal. Consider whether the shipped default is the wrong shape for your work rather than nudging the clamp. ## The spend model Separately from detection thresholds, `annotate_spend` attaches `spend_at_risk_usd` to an incident: the estimated cost of the flagged stretch, with the `spend_basis` that produced it. | Variable | Default | Effect | |---|---|---| | `CLAWMETRY_GUARD_CRITICAL_USD` | `5` | A `warning` above this becomes `critical` | Where no cost is known the value is `0.0` with basis `unknown` — never an invented figure.