Most plugins only need predicates. But if your rule requires per-ref state that isn’tDocumentation 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.
cwd, branch, or env — custom tracker registration via Plugin.trackers adds new state dimensions to the bash AST walk, giving your predicates access to computed values that track across command refs exactly the way the built-in cwd tracker does.
The Tracker<T> and Modifier<T, TAll> API
Trackers come from unbash-walker, re-exported through pi-steering. The shape is:
"cd", "git", "my-tool"). Two scopes are available:
"sequential"— the result becomes the new tracker state for this command AND all subsequent sibling commands in the chain. Use this for state-mutating commands likecd."per-command"— the result applies to this command only. Subsequent commands see the pre-modifier state. Use this for flag overrides likegit -C.
Modifier.apply receives (args, current, allState). The allState parameter is a read-only snapshot of every registered tracker’s pre-ref state, enabling cross-tracker reads. For example, the built-in cwdTracker’s cd modifier reads allState.env to expand $VAR targets. Narrow the TAll type parameter to declare exactly what you need from other trackers.
Returning undefined from apply signals that the modifier cannot statically resolve the result. The walker substitutes the tracker’s unknown sentinel for that command and all that follow, and engine consumers apply an onUnknown: "allow" | "block" policy (default "block", fail-closed).
A minimal custom tracker
ctx.walkerState?.myState is available in every predicate and when.condition callback evaluated for a bash tool call. The tracker key you supply under Plugin.trackers becomes the key on WhenWalkerState.
Built-in trackers
Two trackers ship with the package and are always registered:cwdTracker models the working directory of each command ref. It handles:
cd ABS/cd REL— replace or join with current dircd ~/x,cd(no args) — tilde and bare-cd expansion via$HOMEcd -— no-op (OLDPWD is not tracked)cd "$VAR/pkg"— env-aware expansion viaallState.env(seeenvTrackerbelow)git -C DIR— per-command override; composable:git -C /a -C b pushrecords at/a/bmake -C DIR— per-command; scans all tokens (make parses flags interspersed with targets)env -C DIR— per-command; scans the options region only
envTracker captures statically resolvable shell variable mutations from the same bash chain:
- Bare assignments:
WS_DIR=/ws export NAME=VALUEunset NAME- Seeded from
process.env.{HOME, USER, PWD}at tracker initialization, so~,$HOME,$USER, and$PWDexpand out of the box
pi-steering package root.
cwdTracker known limitations
The cwd tracker is static analysis — it deliberately under- or over-approximates certain shell constructs so callers can make safe policy decisions:
- Dynamic
cdtargets:cd $VAR,cd "$(pwd)",cd $UNDEFINED— the walker readsallState.envto expand$VAR/${VAR}/~. When any part is intractable (command substitution, arithmetic, parameter-expansion with modifiers, unknown var), the modifier returnsundefinedand the walker emits"unknown". ApplyonUnknown: "allow" | "block"(default"block", fail-closed) on yourwhen.cwdpredicate. source/. script.sh— external files are never read.sourceis extracted as a normal command but anycdeffects the sourced script would perform at runtime are opaque to the walker.pushd/popd— not treated ascd.pushd /A && yleavesyat the pre-pushd cwd. Write explicit rules against these commands if you want to catch them.cd -— treated as a no-op; OLDPWD is not tracked.if/casebranches — exactly one branch runs at runtime, so the walker propagates a cwd forward only if ALL branches agree. Otherwise it falls back to the pre-branch cwd. Commands inside each branch still see that branch’s own cwd.while/for/select— the body may iterate zero times. Body cwd never propagates forward; commands inside the body are walked from the loop’s starting cwd.eval "..."— the string argument is not re-parsed. Onlyevalitself is extracted; commands inside the string are invisible.- Background
&— treated like;(cd effects propagate to subsequent commands). In real bash,cd /x &runs in a backgrounded subshell and the parent shell’s cwd is unchanged. This is a deliberate over-match: guardrail consumers see the more conservative cwd andwhen.cwdchecks fire as expected. - Heredoc bodies — heredoc content is treated as data (a redirect payload on the owning command). A
cdwritten inside a heredoc body is never extracted or walked. This is correct behavior, not over-match: heredoc bodies in real bash are stdin, not commands. env -C DIR cmdwrapper-expansion interaction — the outerenvref is recorded atDIR, but the innercmdref surfaced by wrapper expansion has no entry in the walk result; consumers fall back to the session cwd for it. Lifting this requires wrapper expansion to consult the cwd tracker when computing inner refs.
Tracker extensions via Plugin.trackerExtensions
Plugin.trackerExtensions lets you layer additional modifiers onto an existing tracker without replacing it — useful when a tool has a -C-style flag that the built-in tracker doesn’t know about.
{ trackerName: { basename: modifier } }. Name collision on the same basename within the same tracker logs a WARN and keeps the first-registered modifier. Name collision on Plugin.trackers (two plugins claiming the same tracker key) is a hard error — the engine cannot operate safely with two plugins claiming the same state dimension.
resolveWord(word, env) helper
resolveWord is re-exported from the pi-steering package root. It is the same helper cwdTracker’s cd modifier uses internally to expand dynamic targets.
resolveWord expands $VAR, ${VAR}, and ~ (at word start, unquoted) via the supplied env map. It returns undefined when any part of the word is statically intractable — command substitution, arithmetic, parameter-expansion with modifiers, or an unknown variable. Handle undefined with an onUnknown-style policy on your own predicate’s option shape.
envTracker + shell var expansion
The env tracker and cwd tracker compose through allState. A variable set in the same chain becomes available to the cd modifier’s expansion pass at the following cd:
(FOO=/s; cd "$FOO"); cmd — the outer cmd sees neither FOO nor the subshell’s cd effect. The walker handles this generically via subshellSemantics: "isolated" on both trackers.
Out of scope for v0.1.0: readonly, local, declare, typeset, source/., and function-body walking. The envTracker source (trackers/env.ts) lists the full deferred-scope inventory and graduation criteria.
Most plugin authors never need custom trackers — plugin-registered predicates cover 90% of use cases. Reach for
Plugin.trackers only when your rule needs per-ref state that can’t be derived from the command’s args and the existing cwd, branch, or env tracker outputs.