Pi Agent SDK bash PATH and Nix flake purity
Pi Agent SDK bash PATH and Nix flake purity
Section titled “Pi Agent SDK bash PATH and Nix flake purity”Related: Cloud agent orchestrator · 2026-09-26 Herdr for v0 cloud orchestrator
Package boundary (important)
Section titled “Package boundary (important)”| Layer | Package | Owns bash PATH? |
|---|---|---|
| Agent loop / tool calling | @earendil-works/pi-agent-core (was @mariozechner/pi-agent-core) | No — no shell spawn |
| Coding agent / SDK tools | @earendil-works/pi-coding-agent | Yes — createBashTool / createLocalBashOperations |
“Pi Agent SDK” bash PATH work lives in coding-agent, not agent-core. Repo: badlogic/pi-mono / earendil-works/pi. Docs: Environment variables.
What happens on each bash tool call
Section titled “What happens on each bash tool call”Source: packages/coding-agent/src/core/tools/bash.ts, packages/coding-agent/src/utils/shell.ts.
- Optional
commandPrefixis prepended to the model’s command (newline-joined). resolveSpawnContextbuilds env:- Start from
getShellEnv()={ ...process.env }with agent bin dir prepended toPATH/Pathif missing. - Strip stale
PI_SESSION_ID/PI_SESSION_FILE/PI_PROVIDER/PI_MODEL/PI_REASONING_LEVEL, then re-inject from extension context whenexposeSessionEnvironmentis true (default). - Run
spawnHook(ctx)if provided — hook may rewritecommand,cwd, andenv.
- Start from
ops.exec(command, cwd, { env: spawnContext.env, ... })→child_process.spawn(shell, ["-c", command], { cwd, env, detached: true on Unix }).- If
envis omitted at the operations layer, spawn falls back togetShellEnv()again. Supplying any customenvskips that fallback (wrappers must re-apply bin prepend themselves — documented failure mode in community patches).
getShellEnv (exact behavior)
Section titled “getShellEnv (exact behavior)”export function getShellEnv(): NodeJS.ProcessEnv { const binDir = getBinDir(); // join(getAgentDir(), "bin") → ~/.pi/agent/bin by default const pathKey = Object.keys(process.env).find((k) => k.toLowerCase() === "path") ?? "PATH"; const currentPath = process.env[pathKey] ?? ""; const pathEntries = currentPath.split(delimiter).filter(Boolean); const hasBinDir = pathEntries.includes(binDir); const updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter); return { ...process.env, [pathKey]: updatedPath };}So default bash PATH = host process PATH + managed fd/rg bin first. There is no sandbox PATH wipe in stock coding-agent (unlike some OpenClaw forks that set a hardcoded Unix PATH inside a docker sandbox).
Shell binary (separate from PATH)
Section titled “Shell binary (separate from PATH)”getShellConfig(shellPath?): settings shellPath → /bin/bash → bash on PATH → sh. The shell executable can still be system bash even if you later replace child PATH.
What does not set tool PATH
Section titled “What does not set tool PATH”| Knob | Role |
|---|---|
PI_PACKAGE_DIR | Nix/Guix package install root for Pi itself — not bash tool PATH |
PI_CODING_AGENT_DIR | Config + bin location (…/bin prepend) |
Process markers AI_AGENT=pi / PI_CODING_AGENT=true | Identity for children; SDK embed does not set these automatically |
commandPrefix / example source ~/.profile | Mutates command text, not a pure env map — easy to re-pollute PATH |
Making bash use an exclusive Nix flake PATH
Section titled “Making bash use an exclusive Nix flake PATH”Goal: every Pi bash spawn resolves tools only from a flake devShell, not host Homebrew / system /usr/bin leftovers / accidental prepends.
Recommended: replace env in spawnHook
Section titled “Recommended: replace env in spawnHook”Capture once (coordinator or agent bootstrap):
# From project flake root; adjust attr as needednix print-dev-env path:.#devShells.$(nix eval --raw --impure --expr 'builtins.currentSystem') > /tmp/flake.env# or: nix develop -c env -0 / nix print-dev-env '.#default'Then in the custom bash tool / extension:
import { createBashTool } from "@earendil-works/pi-coding-agent";
const flakeEnv = loadPrintDevEnv("/tmp/flake.env"); // parse KEY=VALUE / export linesconst purePath = flakeEnv.PATH; // nix store paths only
const bashTool = createBashTool(projectCwd, { spawnHook: ({ command, cwd, env }) => ({ command, cwd, env: { ...flakeEnv, // NIX_*, SSL_CERT_FILE, PKG_CONFIG_PATH, etc. // Keep Pi session metadata from resolveSpawnContext: PI_SESSION_ID: env.PI_SESSION_ID, PI_SESSION_FILE: env.PI_SESSION_FILE, PI_PROVIDER: env.PI_PROVIDER, PI_MODEL: env.PI_MODEL, PI_REASONING_LEVEL: env.PI_REASONING_LEVEL, PATH: purePath, // exclusive — do not prepend host or ~/.pi/agent/bin unless flake provides those tools }, }),});Exclusive means assign PATH: purePath, not purePath + ":" + env.PATH. Prepend-only still lets host binaries win on name collisions later in the list; replace wins.
Also acceptable: start Pi inside the flake shell
Section titled “Also acceptable: start Pi inside the flake shell”nix develop -c pi (or Herdr pane already in nix develop) makes process.env.PATH flake-pure before getShellEnv. Caveat: getShellEnv still prepends ~/.pi/agent/bin, so purity is broken unless you:
- put
fd/rg(and any managed bins) in the flake, and spawnHookstrips / replaces PATH again, or- point
PI_CODING_AGENT_DIRat a flake-owned agent dir whosebinis itself store-backed.
Weaker: commandPrefix / source
Section titled “Weaker: commandPrefix / source”Official example prepends source ~/.profile to the command. That reintroduces impure profile PATH and is hard to audit. Prefer env replacement over sourcing.
Custom BashOperations
Section titled “Custom BashOperations”You can replace operations.exec entirely (e.g. SSH, remote). Same rule: if you pass env, you own PATH; if you omit it, Pi uses getShellEnv().
Herdr / Cloud Orchestrator implications
Section titled “Herdr / Cloud Orchestrator implications”Herdr supports pi as an agent start --kind and --env KEY=VALUE on launch (CLI). Important constraints:
--envis launch-only — not restored after Herdr server restart (issue #1269, treated as expected). Flake PATH via--env PATH=…alone is fragile across restore.- Pane interactive shell must already be at a prompt before
agent start; that shell’s PATH ≠ Pi bash-tool PATH. Two layers:- Pane / agent process env (how
piis launched) — Herdr--env, wrapper script, ornix develop -c pi - Per-tool bash spawn env — Pi
spawnHook(authoritative for LLMbash)
- Pane / agent process env (how
- For durable reproducibility, prefer a wrapper on PATH that always enters the flake (
exec nix develop … -c pi "$@") plus a Pi extension that forces flakeprint-dev-envon every bash spawn — so restore and tool calls agree even when Herdr drops launch env.
Folded into Cloud agent orchestrator as a post-v0 agent-env lock (v0 still marks Nix packaging out of scope for the coordinator itself).
Practical checklist for exclusive flake PATH
Section titled “Practical checklist for exclusive flake PATH”- Build
flakeEnvfromnix print-dev-envfor the project’sdevShell(cache per project path / flake lock hash). - Register
createBashToolwithspawnHookthat setsenvto flake map + neededPI_*session keys; overwritePATH. - Decide whether
~/.pi/agent/binis allowed; if not, omit it from PATH (and ensurerg/fdcome from the flake if the agent relies on them). - Optionally set
shellPathto a store bash from the flake so even the shell binary is pinned. - Under Herdr: launch via flake wrapper; do not depend on
--env PATHsurviving restore. - Smoke-test:
bashtool runstype -a node,echo "$PATH",command -v gitand assert only/nix/store/…prefixes.
Sources
Section titled “Sources”- pi.dev Environment variables
- bash.ts —
resolveSpawnContext,createBashTool,spawnHook - shell.ts —
getShellEnv,getShellConfig - bash-spawn-hook example
- Herdr CLI /
--env; Herdr #1269 (launch env not restored)