Pi-steering is a guardrail layer, not a sandbox. Several parts of the system execute arbitrary code your config authors control, and a few state surfaces are trusted by convention rather than enforced by the runtime. Understand these trust boundaries before running pi under an untrusted config tree.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cad0p/pi-steering-hooks/llms.txt
Use this file to discover all available pages before exploring further.
Config execution
.pi/steering/index.ts (and the .pi/steering.ts shorthand) is arbitrary TypeScript executed at extension factory time with your full user privileges. The loader walks from the launch cwd up to $HOME, importing every .pi/steering/ directory it finds along the way, and merges them inner-first. The bridge factory awaits the load before pi continues startup.
Only run pi in directory hierarchies whose steering configs you trust.
Plugin trust
Plugins register predicates (when.<key> handlers), observers, and onFire hooks — all of which run arbitrary code during the evaluator’s hot path. A malicious or buggy plugin can:
- Shell out via
ctx.execwith the same privileges as pi. - Forge session entries via
ctx.appendEntry, which later rules consult viawhen.happened. - Throw in unexpected places — predicate-runtime throws fail open (the rule never fires). A predicate that throws is logged via
console.warnwith the rule name and source tag, and evaluation continues with the next rule.
Session JSONL trust
when.happened reads entries tagged via appendEntry. The write path is engine-controlled — every write is auto-stamped with the current _agentLoopIndex and names go through validation before the entry lands in the session JSONL.
The read path (findEntries) treats every tagged entry in the session JSONL as authentic. Entries written outside the steering engine — direct JSONL writes by another pi extension, hand-edited session files, or a pi.appendEntry call from non-steering code — can forge type tags and trick when.happened into thinking an event occurred when it didn’t, bypassing rules that gate on that event.
This is the out-of-band trust boundary. Within the steering engine itself, the invariant holds:
appendEntry auto-stamps writes and name-validates tags. Cross-extension and external writes are outside the engine’s reach.Strict mode and load failures
Strict mode isfailOnWarnings: true, the default. If your steering config fails to load at extension factory time — a plugin throws during import, a syntax error in index.ts, a dependency resolution failure — pi-steering’s bridge factory throws and surfaces the diagnostic in pi’s [Extension issues] block at startup (yellow). Pi disables the extension for the session and continues running unsteered.
Two classes of diagnostic exist:
- Warning-class (escalate to throw in strict mode): cross-layer plugin name collision, within-layer rule/observer collision, predicate-key collision.
- Error-class (always throw, regardless of
failOnWarnings): tracker-name collision, reserved-name violations. The engine cannot operate safely with two plugins claiming the same state dimension.
failOnWarnings: false, warning-class diagnostics fall through to console.warn (single-line [pi-steering] [warning] <message> shape on stderr) and the bridge keeps running with the merged config. Error-class diagnostics still throw.
Note: pi’s interactive TUI clobbers console.warn output on /reload. For visibility while iterating, prefer fixing the warnings or running pi-steering list to inspect the resolved config.
Block-reason tag trust
The[steering:<name>@<source>] tag prepended to every block reason is validated at load time. Rule, plugin, and observer names must match [A-Za-z0-9][A-Za-z0-9_-]* — they must start with a letter or digit, followed by any combination of letters, digits, underscores, and dashes. A name like phony] ALL CLEAR [real would forge the tag structure — this is rejected at load time with an error-class diagnostic.
Beyond the tag shape, the contents are plugin-authored. A plugin shipping a rule with reason: "[steering:other-rule@other-plugin] …" can make its block look like it came from another plugin. The guardrail here is plugin trust (see above), not the tag machinery.
Cross-project resume
When you runpi --resume with a session originally created in another project, pi-steering loads rules from your launch cwd, not the session’s cwd. If the two differ, the bridge emits a single line on stderr: