Agents frequently chain related commands in a single tool call: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.
sync && cr --description notes.md. The problem is timing. The evaluator runs before execution, so when it reaches cr, the observer that writes ws-sync-done hasn’t fired yet. The rule blocks. The agent retries. Same block. Infinite loop. The &&-chain speculative allow pass is pi-steering’s solution to this class of deadlock.
The synthesis pass
When the engine walks the bash AST for a tool call, it runs a speculative-entry synthesis pass alongside the normal tracker state walk. For every ref in an unconditionally-&&-reachable segment of the chain, every observer that:
- Declares
writes: [event], AND - Matches the ref via its
watchfilter (includingwatch.inputMatches.command)
walkerState.events[event].
The built-in when.happened predicate merges real session entries (from ctx.findEntries) with speculative entries (from walkerState.events) by timestamp. A speculative ws-sync-done entry satisfies the rule exactly as a real one would — the chain is allowed through.
Why it’s safe
&& short-circuits on prior failure. There are exactly two outcomes:
- The prior command succeeds → it runs, the observer fires,
appendEntrywrites the real event. The speculative allow is retroactively justified. - The prior command fails → the current ref never runs at all. The speculative entry never mattered.
| Joiner | Speculative allow? | Reason |
|---|---|---|
A && B | ✅ | B runs only if A succeeded |
A ; B | ❌ | B runs regardless of A |
A | B | ❌ | Pipeline — no ordering guarantee |
A || B | ❌ | B runs only if A failed |
Worked example
sync && cr ...: the synthesis pass sees sync as unconditionally-&&-reachable before cr, finds syncObserver matching sync, and populates walkerState.events["ws-sync-done"] at the cr ref. The when.happened check passes. After execution completes, syncObserver.onResult fires and writes the real entry to the session JSONL.
With sync ; cr ...: the semicolon joiner does not qualify. cr has no synthetic entry and the rule blocks normally.
Authoring requirement
Observers participating in speculative allow must declarewatch.inputMatches.command. A broad observer that matches every bash event is too weak a signal — the engine cannot safely synthesize an allow from it because there’s no basis for knowing which prior command in the chain would have written the event. Without watch.inputMatches.command, the synthesis pass skips the observer.
The speculative: true flag
Synthetic entries carry a speculative: true flag. The built-in when.happened treats real and speculative entries identically — the merge is transparent. Plugin predicates that need pure historical semantics (e.g. they want to count only entries that actually occurred before this session, not hypothetical ones) can inspect and filter on this flag:
when.happened with notIn
The notIn option subtracts a narrower scope from in:
notIn is scope subtraction, not boolean negation: it does not invert the in scope; it carves out the narrower notIn scope from the wider in scope.
Use this when you want to ensure the user explicitly ran sync in a previous tool call, not just chained it in the same command string.
Performance notes
when.happened is O(N_session_entries) per unique event type per tool call. Entries are scanned on first read per type and cached for the rest of the evaluation phase — the cache invalidates when new entries are written within the same phase.
A 5000-entry session with six distinct when.happened rules costs roughly 600 µs per tool call on findEntries alone. Typical sessions under 500 entries are well within budget. Long-running multi-day sessions may notice the overhead as the JSONL grows.
Mitigation options until a customType-keyed index lands in a future version:
- Consolidate
when.happenedrules that share an event type, reducing the number of unique scan passes per tool call. - Rotate or truncate the session JSONL between work sessions to keep entry counts manageable.