跳转到内容

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


LayerPackageOwns 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-agentYes — 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.


Source: packages/coding-agent/src/core/tools/bash.ts, packages/coding-agent/src/utils/shell.ts.

  1. Optional commandPrefix is prepended to the model’s command (newline-joined).
  2. resolveSpawnContext builds env:
    • Start from getShellEnv() = { ...process.env } with agent bin dir prepended to PATH / Path if missing.
    • Strip stale PI_SESSION_ID / PI_SESSION_FILE / PI_PROVIDER / PI_MODEL / PI_REASONING_LEVEL, then re-inject from extension context when exposeSessionEnvironment is true (default).
    • Run spawnHook(ctx) if provided — hook may rewrite command, cwd, and env.
  3. ops.exec(command, cwd, { env: spawnContext.env, ... }) → child_process.spawn(shell, ["-c", command], { cwd, env, detached: true on Unix }).
  4. If env is omitted at the operations layer, spawn falls back to getShellEnv() again. Supplying any custom env skips that fallback (wrappers must re-apply bin prepend themselves — documented failure mode in community patches).
packages/coding-agent/src/utils/shell.ts
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).

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.

KnobRole
PI_PACKAGE_DIRNix/Guix package install root for Pi itself — not bash tool PATH
PI_CODING_AGENT_DIRConfig + bin location (…/bin prepend)
Process markers AI_AGENT=pi / PI_CODING_AGENT=trueIdentity for children; SDK embed does not set these automatically
commandPrefix / example source ~/.profileMutates 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.

Capture once (coordinator or agent bootstrap):

Terminal window
# From project flake root; adjust attr as needed
nix 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 lines
const 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
  • spawnHook strips / replaces PATH again, or
  • point PI_CODING_AGENT_DIR at a flake-owned agent dir whose bin is itself store-backed.

Official example prepends source ~/.profile to the command. That reintroduces impure profile PATH and is hard to audit. Prefer env replacement over sourcing.

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 supports pi as an agent start --kind and --env KEY=VALUE on launch (CLI). Important constraints:

  1. --env is launch-only — not restored after Herdr server restart (issue #1269, treated as expected). Flake PATH via --env PATH=… alone is fragile across restore.
  2. 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 pi is launched) — Herdr --env, wrapper script, or nix develop -c pi
    • Per-tool bash spawn env — Pi spawnHook (authoritative for LLM bash)
  3. For durable reproducibility, prefer a wrapper on PATH that always enters the flake (exec nix develop … -c pi "$@") plus a Pi extension that forces flake print-dev-env on 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”
  1. Build flakeEnv from nix print-dev-env for the project’s devShell (cache per project path / flake lock hash).
  2. Register createBashTool with spawnHook that sets env to flake map + needed PI_* session keys; overwrite PATH.
  3. Decide whether ~/.pi/agent/bin is allowed; if not, omit it from PATH (and ensure rg/fd come from the flake if the agent relies on them).
  4. Optionally set shellPath to a store bash from the flake so even the shell binary is pinned.
  5. Under Herdr: launch via flake wrapper; do not depend on --env PATH surviving restore.
  6. Smoke-test: bash tool runs type -a node, echo "$PATH", command -v git and assert only /nix/store/… prefixes.