Configuration reference
September 20, 2026 · View on GitHub
Every key, its default, what a project file may change, the status line templates, and the update note. The README covers the common cases; guards.md explains what each threshold does.
Contents: User config · Project config · Environment · Status line and trace sidebar · After updating the package
User config
User file ~/.pi/agent/pi-warden/config.json (owner-only). /warden config opens it in Pi's editor. Missing keys use these defaults:
{
"enabled": true,
"typesafe": false,
"mode": "steer",
"timeoutMs": 5000,
"maxRequests": 500,
"action": {
"enabled": true,
"tools": ["bash", "powershell", "ctx_execute", "ctx_batch_execute", "ctx_execute_file", "write", "edit"],
"failOpen": true,
"irreversible": { "warn": 0.5, "confirm": 0.7 },
"offTask": { "warn": 0.6, "steer": 0.85 },
"intentMismatch": 0.9,
"visibleMismatch": 0.8,
"shouldProceed": { "hold": 0.6 },
"feedbackLog": true,
"commandRules": [],
"commandDenyRules": [],
"exemptRules": [],
"pathRules": [],
"armingRules": []
},
"rules": {
"enabled": true,
"threshold": 0.7,
"files": [],
"fallback": true,
"maxChars": 8000,
"exclude": [],
"skip": [],
"sensitivePaths": {}
},
"slop": {
"enabled": true,
"threshold": 0.7,
"prose": { "enabled": true, "audience": "technical", "threshold": 0.7, "trend": 2, "minChars": 200 }
},
"security": { "enabled": true, "threshold": 0.7 },
"stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
"done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
"runaway": { "enabled": true, "repeats": 4, "thinkingRepeats": 10, "minChars": 400, "recover": true },
"notify": { "enabled": false, "cooldownMs": 10000, "command": [] },
"subagent": { "enabled": true, "wake": true, "threshold": 0.8, "cooldownMs": 120000 },
"widget": { "enabled": true, "placement": "aboveEditor", "shortcut": "ctrl+shift+w", "panelWidth": "40%" },
"steerVisible": false,
"notices": false,
"steerBudget": 3,
"typesafeBackend": "typesafe"
}
| Key | Meaning |
|---|---|
enabled | Master switch for the extension. |
typesafe | Consent to send requests to TypeSafe. Set by /warden enable; only the user file or PI_WARDEN_ENABLED=1 can grant it. |
typesafeBackend | The judgment service: "typesafe" (default) or "openrouter". User file only — a project must not redirect judgments. |
mode | steer (hold goes back to the agent), confirm (dialog for you), advise (never holds). |
timeoutMs | Per-request timeout. On timeout the call is allowed with a warning when action.failOpen is true. |
maxRequests | Per-session request budget. When spent, pi-warden says so once and continues with offline checks. |
action.tools | Tools the action guard inspects. Add your own shell-like tools here. |
action.irreversible | warn and confirm (hold) thresholds on P(irreversible). |
action.offTask | warn and steer thresholds on P(off-task). Off-task never holds. |
action.intentMismatch | P(call differs from the agent's stated plan) that warns and tells the agent, on calls that can change something. |
action.visibleMismatch | Lower mismatch threshold for commands whose effect is visible outside the working tree (commit, push, publish, install, launch). |
action.shouldProceed | { hold } threshold. When P(should_proceed) drops below hold (default 0.6), the agent is steered to pause and ask the user. Steers never holds. Calibrated: AUC 0.07 (inverted) against regret, 0.64 against rejected turns. |
action.feedbackLog | Write each judged call and its outcome to ~/.pi/agent/pi-warden/holds/; never the command. |
action.commandRules | User-defined command rules: { id, pattern, severity: "warn" | "confirm" | "deny", action?, message?, caseSensitive? }. Patterns match the data-text-stripped command, so heredoc bodies and commit messages do not fire them. confirm defaults to action: "dialog" (a prompt for you, in every mode); action: "hold" restores steer semantics. Patterns are JavaScript regexes matched against full commands, so a pattern with nested quantifiers can be slow on long commands — a pathological one is self-inflicted. User file only. |
action.commandDenyRules | The same shape, shorthand for severity: "deny": the call is blocked with no dialog and no TypeSafe request. User file only. |
action.pathRules | User-defined path rules: { id, paths, access, tools, action, message?, onlyIfExists?, regex? }. paths are globs (** any depth, * one segment, ? one character, ~ expands; regex: true reads them as regexes). access is the dimension: "none" any touch matches, "read" writes are held and reads flow, "write" reads are held and writes flow (an append-only log). tools picks the surface: file tools by name check the structured path field; "*" also matches bash commands — a none rule fires on a mention anywhere in the data-text-stripped command, and the write side is matched only at redirect (>, >>) and tee targets, never in arbitrary argv. action: note (the default; the agent is told after the fact), warn, confirm (a dialog, in every mode), block (deny, no dialog). onlyIfExists defaults true, so phantom paths do not fire. A read-scoped rule needs read in action.tools, which does not inspect the read tools by default. User file only. |
action.armingRules | User-defined arming rules: { id, when: { edited, regex?, tools? }, arms: { command, for?, caseSensitive? }, action, message? }. Editing a file matching a when.edited glob arms the rule's arms.command regex for arms.for (default "10m"; accepts "30s", "2h", or ms). While armed, commands matching the regex fire the rule's action: confirm (dialog), hold (steer), block (deny). State lives for the rule's window within a session, refreshed on each matching edit, cleared on session_start, and shown in /warden status. Approving the dialog approves that call, not the window; the dialog re-fires for each matching command while the rule is armed. User file only. |
action.exemptRules | Built-in or user rule ids to exempt, e.g. ["infra-destroy"] for a workflow whose kubectl delete is routine; also rm-recursive / rm-rf / rm-recursive-dangerous-target (the rm classifier) and sensitive-path. An id naming nothing is inert and reported once at startup. Exempting sensitive-path removes the only deterministic credential-touch signal, Jev questions aside. User file only. |
action.escalationThreshold | Jev confidence above which a violation's severity is escalated in the blast-radius and rules-guard escalation paths. Default 0.85. Escalation fires when confidence strictly exceeds this threshold; setting it to 1 effectively disables escalation since noul confidence cannot exceed 1. |
rules.* | Rules source, threshold, path globs, sensitive-path notes. See guards.md → Rules. |
slop.* | Code slop threshold and reply (prose) checks. prose.audience is technical, plain, or free text. |
security.threshold | Written-code risk and tool-output injection threshold. |
stuck.* | Window of tool results kept, failures before a check, cooldown between checks, same-strategy threshold. |
done.* | Completion-claim threshold and whether the agent gets a follow-up turn. |
context.* | Compression thresholds, retention confidence, duplicate size, recall tool. |
runaway.* | Repeat counts that abort a reply, minimum size, whether the agent gets one recovery turn. |
notify.* | Desktop notifications, cooldown, optional relay command (user file only). |
subagent.enabled | Read async subagent reports at all. false ignores them, as before 0.14. |
subagent.wake | Ask Jev whether a report that names trouble deserves a wake. false keeps the offline layer, which never wakes. |
subagent.threshold | P(report needs the agent awake) that wakes it. Conservative on purpose. |
subagent.cooldownMs | At most one batched wake per window, so several children finishing together cost one interruption. |
widget.* | Status line placement, sidebar shortcut and width, per-guard text templates (below). |
learning.adaptiveThresholds | Learn from hold outcomes and suggest threshold adjustments via /warden recommend. |
learning.patternAnalysis | Analyze hold patterns and generate recommendations. |
learning.retentionDays | Days to keep hold records in SQLite before pruning. Records older than this are deleted on startup. 0 disables pruning. Default: 365. |
steerVisible | Show steer messages in the transcript instead of only in the trace panel. |
notices | Print the per-call warning notices (warden · …) in the transcript. Off by default; the widget, the trace panel, and /warden trace always show every event. |
steerBudget | Steers delivered to the agent per run before further non-critical ones are recorded in the trace only. Every delivered steer costs at least one LLM turn, and a closing run that collects six notices collects six restatements of the final status. 0 disables the budget. Critical guards (stuck, done, runaway recovery, subagent wake) always deliver. |
Project config
A project may add .pi/pi-warden.json with enabled and per-guard overrides: stricter thresholds, extra guarded tools, rules.files, rules.skip, rules.sensitivePaths, or "done": { "enabled": false }. Project files are read only when Pi trusts the project. They can never grant typesafe consent, change mode, raise timeoutMs or maxRequests, or set notify.command.
A wince-style setup for a backend repo (the full version is examples/pi-warden.json):
{
"rules": {
"skip": ["tests/**", "**/*.test.*", "docs/**", "**/*.md"],
"exclude": ["secrets/**", "**/*.pem"],
"sensitivePaths": {
"migrations/**": "This touches a migration: tell the user and add a rollback path",
"**/permissions*": "Access control changed: ask the user for a security review before merging"
}
}
}
Environment
| Variable | Effect |
|---|---|
TYPESAFE_API_KEY | Takes precedence over the key stored by /warden enable or /typesafe login. |
OPENROUTER_API_KEY | API key for the OpenRouter backend. Required when typesafeBackend is "openrouter". |
PI_WARDEN_ENABLED=1 | Grants consent for headless runs (same as "typesafe": true). |
PI_WARDEN_MODE=steer|confirm|advise | Overrides mode. |
Status line and trace sidebar
The line above the editor shows the latest verdict per guard. The verdict leads as a chip, the guard follows, and the body reads as data:
OK rules · prose · done
WARN action write · irreversible 0.09 · off-task 0.95 · unrelated · slop: none · off task
context bash · duplicate · saved 1024 bytes
A verdict the guard found nothing in (ok, allow, skipped) folds into one line per verdict naming the guards that spoke, so a quiet turn costs one line instead of one per guard. A quiet verdict keeps its own line when the line names a finding or a caveat (typesafe error, user approved, slop: <symptom>, patterns: <id>), because folding it would report a verdict the guard did not give. The worst verdict sits last, nearest the editor. Folded detail is not lost: /warden status prints the raw line per guard under Last:, and the sidebar keeps every event with its scores.
/warden trace, ctrl+shift+w, and a click on the line each toggle a right-hand sidebar with the full trace, newest first, live. The sidebar does not take the keyboard; click inside it for arrow keys and PgUp/PgDn, c clears, Esc hands input back, q closes. widget.panelWidth sets its width.
Clicks and the wheel need Pi's fullscreen mode (tuiMode: "fullscreen" in /settings). In macOS Terminal.app enable View → Allow Mouse Reporting.
Templates in config.widget control the text. Segments are separated by ·; a segment whose token has no value is dropped. A template should end on {level} or {status}: that trailing word becomes the chip. A template that keeps the level mid-line gives the line no chip, and the guard name leads it instead:
"widget": {
"action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop: {slop} · patterns: {patterns} · {flags} · {level}",
"rules": "warden · rules · {tool} {path} · {asked} rules · {violations} · {status}",
"stuck": "warden · stuck · {failures} failures · same strategy {sameStrategy} · change {approachChange} · progress {progress} · {flags} · {status}",
"done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
"prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}",
"security": "warden · security · {tool} · injection {injection} · exfiltration {exfiltration} · {status}",
"context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes",
"runaway": "warden · runaway · {kind} · {count}× repeated · {chars} chars · {signal} · {status}",
"subagent": "warden · subagent · {agent} · {kind} · {wake} · {status}"
}
Tokens per guard:
| Guard | Tokens |
|---|---|
| action | tool level source irreversible offTask scope approved intent visible plan slop slopStub slopComments slopDead slopHedging patterns reasons path model ms flags time |
| rules | tool path asked violations status source reasons model ms flags time |
| prose | wordy cliches jargon status reasons model ms flags time |
| stuck | failures sameStrategy approachChange progress status source reasons model ms flags time |
| done | changes checks checksPassed claimsDone claimsVerified checksApply outcome status reasons model ms flags time |
| security | tool injection exfiltration status |
| context | tool retention bytesSaved |
| runaway | kind count chars signal block status time |
| subagent | agent kind wake status time |
"enabled": false hides the line; "shortcut": "" disables the keybinding.
After updating the package
Restart Pi after an update; /reload re-imports the entry module but can leave older modules of the same package in memory. Since 0.5.2 the extension checks the shape of the config it receives; a section that an older module does not know (the symptom of two package versions in one process) switches that guard off and prints one warning naming the sections and the schema numbers. 0.9.0 crashed instead when the shape-check module itself was the stale one; since 0.9.1 the extension guards the sections it reads in its own module, so the warning appears and everything else keeps working. If you see the warning, restart Pi.