Skip to main content

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.

pi-steering does not require a single config at a fixed location. Instead, the loader walks up from the launch cwd toward $HOME, importing every .pi/steering/index.ts (or .pi/steering.ts shorthand) it finds along the way. Each directory that contains a config file contributes one layer. Layers are collected inner-first — the directory closest to where you launched pi is the innermost layer and has the highest priority on name collisions.

Discovery order

The loader uses ancestorChain(cwd), which walks from resolve(cwd) up to $HOME (or the filesystem root when $HOME is outside the ancestry). The returned list is always inner-first:
[launch_cwd, parent, grandparent, ..., $HOME]
Each directory in the chain is checked for a .pi/steering/index.ts or .pi/steering.ts file. Directories with neither are skipped silently. Directories with both emit a layer-form-coexistence warning and use the directory form.

Layer merge semantics

Once all layers are collected, buildConfig merges them with these rules: Rules are merged by name, inner layer first. The inner layer’s version of a rule replaces an outer layer’s rule with the same name — this is the primary customization path for monorepo packages that need to tighten or soften a workspace-level rule without touching the shared config. Within a single layer, a duplicate rule name emits a rule-name-collision warning (authoring mistake); cross-layer shadowing is intentional and silent. Plugins are merged by name, first-registered wins (inner layer is first). A cross-layer plugin name collision emits a plugin-name-collision warning — both layers are registering the same plugin, and one of them is redundant. A plugin whose name appears in the union’d disabledPlugins set is exempt from collision detection (adding it to disabledPlugins is itself the resolution). Observers follow the same semantics as rules: inner layer’s observer replaces an outer layer’s observer with the same name; within-layer duplicates warn. disabledRules and disabledPlugins are union’d across all layers. An outer layer that disables no-force-push keeps that disable even when an inner layer does not mention it. Boolean fields (defaultNoOverride, disableDefaults, failOnWarnings) use inner-wins: the innermost layer that explicitly sets the field wins; layers that omit it are skipped.

Monorepo example

Consider this layout:
$HOME/.pi/steering/index.ts              ← outermost (org-level policy)
my-monorepo/.pi/steering/index.ts        ← workspace-level rules
my-monorepo/packages/api/.pi/steering/index.ts   ← package-level (innermost)
Launching pi from my-monorepo/packages/api/ loads all three layers and merges them. Rules defined in the api package config take precedence over the workspace and org configs. A rule with the same name in all three layers uses the api package’s version. This lets teams ship org-wide safety rails in $HOME/.pi/steering/index.ts, add project-specific guards in the repo’s root config, and let individual packages override or tighten rules for their own constraints — all without any of the layers knowing about each other.

CWD-scoped rules

A rule that should apply only within a specific directory tree uses when: { cwd: <pattern> }. The walker resolves the effective cwd per-ref from cd constructs in the same &&-chain, so cd ~/personal && git commit evaluates against ~/personal. Dynamic targets (cd "$WS_DIR/pkg") resolve through the walker’s env tracker seeded from process.env.{HOME, USER, PWD}. The following example, drawn from the no-amend example in the source, shows a cwd-scoped variant that only fires inside a specific subtree:
// In $HOME/.pi/steering/index.ts — only apply to personal projects
import { defineConfig } from "pi-steering";

export default defineConfig({
  rules: [
    {
      name: "personal-no-push-main",
      tool: "bash",
      field: "command",
      pattern: /^git\s+push/,
      when: { cwd: /\/personal\// },
      reason: "Push to a feature branch for personal projects.",
    },
  ],
});
This rule fires only when the effective cwd contains /personal/. Commands run from ~/work/ or any other path pass through untouched. For situations where the cwd cannot be statically resolved (e.g. cd $(pwd), cd $UNDEFINED), the built-in when.cwd predicate applies an onUnknown policy. The default is "block" — fail-closed, the rule fires. To opt into fail-open for dynamic directories, use the spread form:
when: { cwd: { pattern: /\/personal\//, onUnknown: "allow" } }

Cross-project resume

When you pi --resume a session originally started in a different project, pi-steering loads rules from the launch cwd, not the session’s original cwd. If those differ, the bridge emits a single warning on stderr:
[pi-steering] session cwd (/original/project) differs from launch cwd (/current/dir). Steering rules loaded from launch cwd; session-cwd rules NOT applied. To use session-cwd rules, exit pi and re-launch from /original/project.
The bridge then continues with the launch-cwd rules. This is expected behavior: the loader’s walk-up chain is anchored to where pi was started, not to where a previous session began. To use the resumed session’s original project rules, exit pi and re-launch from that project’s directory.
Symlinks in the walk-up chain are followed. If a directory in the ancestry contains a symlink to .pi/steering/, the loader imports that symlinked config as if it were placed in that directory directly. A symlinked config that lands in an unexpected directory executes with full user privileges at that location — this is intentional (it’s how shared org configs work) but worth keeping in mind when auditing your directory hierarchy.
Loading .pi/steering/index.ts executes arbitrary TypeScript with your full user privileges. The loader walks up from the launch cwd to $HOME, importing every config layer it finds. Symlinks in the walk-up chain are followed. This is equivalent to running node -e '...' with each config file as the script.Only run pi in directory hierarchies whose steering configs you trust. A malicious .pi/steering/index.ts placed in any ancestor directory of your launch cwd will be imported and executed at extension factory time.

Build docs developers (and LLMs) love