← Pi Agents

Shells, Processes, and Environment Variables

Pi runs each shell command in a fresh non-interactive shell, so aliases and shell rc files are absent unless you configure shellPath and shellCommandPrefix.

You ask Pi to run ll, and it reports ll: command not found. You press up in your own terminal and ll works. Nothing is broken. Pi started a different shell.

shell-aliases.md states the cause in the first line: Pi starts a separate non-interactive shell process for each Bash command. Non-interactive Bash does not expand aliases by default and usually does not load the same startup files as an interactive terminal.

Which shell runs what

Command source Shell
Model calls the built-in bash tool Pi’s resolved Bash executable
You enter !command or !!command The same resolved Bash executable
Model calls the optional powershell tool PowerShell 7 (pwsh.exe) or Windows PowerShell
An extension provides or replaces a shell tool The operations implemented by that extension

The last row is the one that catches people. If an extension replaces the bash tool, shellCommandPrefix no longer configures it — shell-aliases.md says so directly under its troubleshooting section.

Resolution order

Pi normally invokes Bash with bash -c. On Unix it tries /bin/bash, then bash on PATH, then sh when Bash is unavailable. On native Windows it checks the configured path first, then Git Bash, then bash.exe on PATH.

That last fallback is why shopt fails. If shopt is not found, either Pi fell back to sh or shellPath points at a non-Bash shell. Install Bash or point shellPath at one before using Bash-specific setup.

Point at a real Bash

{
  "shellPath": "C:\\Program Files\\Git\\bin\\bash.exe"
}

settings.md documents shellPath as defaulting to the platform default and supporting a leading ~. On Windows, use forward slashes or escape backslashes, because JSON uses backslashes for escape sequences. windows.md shows the same rule with a Cygwin path.

Run /reload after changing the setting.

Setup that runs before every command

shellCommandPrefix is prepended to both the built-in bash tool and user-entered ! and !! commands. Pi joins the prefix and the requested command with a newline, and runs the prefix again for every command — so keep it fast and free of interactive prompts.

{
  "shellCommandPrefix": "export CI=1"
}

Two documented failure modes follow from “runs every time”. If a setup command waits for input, it will hang every command; remove interactive commands from the prefix. And because it is non-interactive, output that depends on a terminal will misbehave.

Make aliases work

Store the aliases Pi needs in a Bash-compatible file rather than parsing your interactive shell configuration:

# ~/.bash_aliases
alias ll='ls -la'
alias gs='git status --short'

Then enable expansion and source the file:

{
  "shellCommandPrefix": "shopt -s expand_aliases\nsource ~/.bash_aliases"
}

The \n is a real newline in the JSON string, which Pi joins with the command. Verify it from inside Pi:

!ll

The output should match ls -la.

Do not source a .zshrc into Bash. shell-aliases.md warns that zsh options, functions and plugins may not parse or behave correctly there. If Pi’s model must run a command that only exists in your interactive setup, the honest fix is a script on PATH, not a shim in the prefix.

The session environment handed to commands

environment-variables.md documents five variables injected into commands run by the bash and powershell tools:

Variable Description
PI_SESSION_ID Current session ID
PI_SESSION_FILE Absolute path to the current session JSONL file; unset for ephemeral sessions
PI_PROVIDER Currently selected model provider
PI_MODEL Currently selected model ID
PI_REASONING_LEVEL Current effective reasoning level

The values are resolved when each command starts, so switching models or changing the reasoning level affects the next shell command without restarting Pi. PI_PROVIDER and PI_MODEL identify the selected Pi model, not a different upstream model a router may choose internally.

The documentation’s advice for the model is worth copying verbatim into your own instructions: when asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt.

Wrong: “Pi’s commands run in my terminal, so if it works for me it works for Pi.” Correct: the executable is the same resolved Bash, but the environment is not. Session variables are injected into the LLM-callable bash and powershell tools and are not injected into user-entered ! or !! commands, so a check you run by hand may see a different environment than the same check run by the model.

Process markers for children

Separate from the session variables, the CLI and RPC entry points set two markers that child processes inherit:

  • AI_AGENT=pi — a generic marker so tooling can identify Pi as the launching agent.
  • PI_CODING_AGENT=true — Pi-specific, for processes that need to detect they run inside Pi.

They are not session-specific, and they are not set automatically when Pi is embedded through the SDK.

Windows: two shells, deliberately

windows.md gives the choice plainly. Native Windows uses Git Bash by default and can optionally expose PowerShell to the model. Pi inside WSL uses the Linux environment and its Bash installation.

To let the model use PowerShell, replace the model-facing shell:

{
  "defaultTools": ["read", "powershell", "edit", "write"]
}

["-bash", "+powershell"] does the same while keeping other default tools you configured. The powershell tool starts PowerShell with -NoProfile -NonInteractive -ExecutionPolicy Bypass, falling back from pwsh.exe to Windows PowerShell. Administrator-enforced execution policies can still take precedence.

Your own ! and !! editor commands continue to use Bash even after this change, and the powershell tool is available only when Pi runs as a native Windows process.

Verify it from inside Pi

The check has to run the way the model will run it, as a tool call:

Run this and show me the output, unmodified:
printf 'shell=%s\n' "$BASH_VERSION"; printf 'model=%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"

If $BASH_VERSION comes back empty, Pi is not running Bash and any Bash-only setup you configured is being ignored. Running the same command yourself with ! proves less than it looks: that checks your own environment, not the tool’s.

The environment is now predictable, which is the precondition for the only part of a run you fully control. Pi will not come back and ask what “done” means, so that has to be settled before the request goes in.