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.
unbash-walker is the bash AST utility that pi-steering builds on. It provides command extraction, wrapper expansion, and an extensible per-command state tracker. Plugin authors who need to write custom trackers or call the walker directly can import from pi-steering, which re-exports everything from unbash-walker for forward compatibility — imports from the package root will not break if unbash-walker is ever published separately.
Import
Quick look
parse(source: string): Script
Re-exported from unbash. Parses a bash string into a Script AST node. The Script is the root of the unbash tree that all other unbash-walker functions consume.
extractAllCommandsFromAST(script, source): CommandRef[]
Flattens a Script into the ordered list of CommandRefs that actually run. Unlike a naive tree walk, this function handles the full range of bash control structures:
AndOr(&&/||chains)Pipeline(|chains)Subshell((...))BraceGroup({...})- Control flow (
if,while,for,case,select) - Command substitution inside words (
$(...),`...`,<(...))
Parameters
The parsed Script AST produced by
parse(source).The original bash source string. Stored on every returned
CommandRef so
position offsets within the AST node resolve to the correct characters.CommandRef objects in AST order.
expandWrapperCommands(commands): { commands: CommandRef[] }
Given a list of extracted CommandRefs, recursively reveals sub-commands of wrapper programs — shells invoked with an inline -c script, privilege escalators, and process wrappers that run another command as their argument.
| Wrapper | How sub-commands are found |
|---|---|
sh -c '...', bash -c '...', zsh -c '...' | Parses the -c argument as a new Script and recurses |
sudo CMD, env CMD | Treats the first non-option argument as the wrapped command |
xargs CMD | Treats arguments after options as the wrapped command |
nice CMD, nohup CMD, strace CMD | Same as sudo |
find -exec CMD ; / find -exec CMD + | Extracts the -exec clause |
fd -x CMD / fd --exec CMD | Same as find -exec |
git push --force fires whether the command appears directly or wrapped inside sudo sh -c 'git push --force'.
Extending the wrapper registry
The registryexpandWrapperCommands consults is exported as WRAPPER_COMMANDS and can be extended by plugin authors or test harnesses:
walk(script, initialState, trackers, refs?): WalkResult<T>
Thread a registry of named state trackers through the AST. Returns a WalkResult<T> (= Map<CommandRef, T>) from each CommandRef to a snapshot carrying every tracker’s value at that command — after all preceding sequential modifiers have been applied.
Parameters
The parsed Script AST.
Starting values for each tracker. Any key missing here falls back to the
tracker’s
initial field. Typically callers pass { cwd: process.cwd() }
to seed the cwd tracker from the session’s working directory.Named tracker registry. Each key names a tracker dimension (e.g.
"cwd",
"env", "branch"). Registration order matters — trackers registered
earlier see the pre-ref snapshot from later trackers when reading allState.
Register envTracker before cwdTracker so the cwd tracker’s cd modifier
can expand $VAR references via the env map.Pass your own
readonly CommandRef[] (from a prior extractAllCommandsFromAST call)
to get the returned Map keyed by the same ref objects — enabling
pointer-equality lookups. Omit to have walk extract refs internally.Isolation and propagation semantics
| Construct | Behavior |
|---|---|
Sequential modifiers (cd, git checkout) | Update the tracker’s threaded value — propagates to all subsequent sibling commands |
Per-command modifiers (git -C, make -C) | Update the command’s snapshot only — threaded value unchanged |
Pipeline (A | B) | Each peer runs in its own subshell; no cross-peer propagation |
Subshell ((A; B)) | Respects each tracker’s subshellSemantics; "isolated" (default) means changes don’t escape |
Brace group ({A; B}) | Not a subshell — sequential changes propagate out |
if / case branches | One branch runs at runtime; walker propagates a value only if all branches agree — otherwise falls back to the pre-branch value |
while / for / select | Body may run zero times; body cwd never propagates forward to commands after the loop |
cwdTracker
Built-in tracker for the effective working directory of each command. Models all common bash cwd-changing constructs and exposes them as per-command snapshots for the when.cwd predicate and plugin predicates.
What it models
| Pattern | Effect |
|---|---|
cd /absolute/path | Sets cwd to the absolute path |
cd relative/path | Resolves relative to current cwd |
cd ~ or cd (no args) | Expands ~ via envTracker.HOME |
cd ~/subdir | Expands ~ and appends the suffix |
cd - | No-op (OLDPWD not tracked) |
git -C /dir CMD | Per-command cwd override for the git invocation |
make -C /dir target | Per-command cwd override for make |
env -C /dir CMD | Per-command cwd override for env-prefixed commands |
Dynamic cd "$VAR" | Resolved via envTracker if $VAR is statically known; "unknown" sentinel otherwise |
"unknown" sentinel
When the walker can’t statically resolve a cd target — due to command substitution ($(cmd)), arithmetic, parameter-expansion with modifiers, or an env variable not in the current envTracker state — the tracker emits the string "unknown" for all subsequent commands in that scope. The engine’s when.cwd predicate applies onUnknown: "allow" | "block" (default "block", fail-closed) to decide how to treat the sentinel.
Known limitations
pushd/popdare not modelled as cwd changes — they are extracted as normal commands.git --git-dir=/pathandgit --work-tree=/pathare not modelled (narrower than-C; planned follow-up).eval "..."— the string argument is not re-parsed; commands inside are invisible.source script.sh/. script.sh— external files are never read; anycdeffects they would perform are opaque.
envTracker
Built-in tracker for the shell environment map at each command. Seeded from process.env.{HOME, USER, PWD} at initialization; captures statically resolvable assignments from the command stream.
What it captures
| Pattern | Effect |
|---|---|
FOO=bar (bare assignment) | Sets FOO to "bar" |
export FOO=value | Sets FOO to "value" |
unset FOO | Removes FOO from the map |
What it does not capture
readonly FOO=value,local FOO=value,declare/typeset— attribute-bearing assignments.source/.— opaque file inclusion.- Function-body walking — function definitions are walked when defined, not when invoked.
FOO+=value(append),FOO=(a b c)(array init),FOO[0]=value(array-index) — compound forms.
"isolated" — env changes inside (A; B) don’t escape to the outer scope.
resolveWord(word: Word, env: EnvState): string | undefined
Statically resolve an unbash Word to its string value, expanding $NAME, ${NAME}, and ~ via the supplied env map.
undefined when any part of the word is intractable — command substitution, arithmetic, parameter-expansion with modifiers, or a variable not present in env. Shared by cwdTracker’s cd modifier and reusable by plugin predicates that need to expand arguments from ctx.input.args.
Helper functions
getCommandName(cmd: CommandRef): string
Returns the full command name as it appears in source, including any leading path (e.g. /usr/bin/git).
getBasename(cmd: CommandRef): string
Strips any leading path from the command name. /usr/bin/git → "git". Use this when matching command names in predicates — agents and scripts commonly invoke tools by full path.
getCommandArgs(cmd: CommandRef): Word[]
Returns the argument words after the command name. For git commit -m "fix" this gives [Word("-m"), Word("fix")]. Each Word carries .value (lexical, quote-stripped) and .text (raw source token).
isBareAssignment(cmd: CommandRef): boolean
Returns true for commands that are purely shell variable assignments with no command name — e.g. FOO=bar. The envTracker uses this to distinguish assignment-only commands (which never have a basename) from prefix assignments on a real command (FOO=bar myprogram).
formatCommand(cmd: CommandRef, options?): string
Re-serializes a CommandRef as a single-line display string with length-aware shrinking and path-aware elision. Useful for including the command in block reason messages.
CommandRef type
Tracker<T> and Modifier<T, TAll> types
Plugin authors implement these to introduce new walker state dimensions via Plugin.trackers.
allState is a read-only snapshot of every registered tracker’s pre-ref state. Use the TAll type parameter to declare the cross-tracker reads your modifier needs:
undefined from apply to signal that the new value can’t be resolved statically. The walker substitutes the tracker’s unknown sentinel and (for sequential modifiers) stops refining that dimension for subsequent commands in the current scope.