← Pi Agents

Shells, Processes, and Environment Variables

Pi runs shell commands in separate non-interactive processes. Learn how shell selection, startup files, command prefixes, and session environment variables affect what the model can execute.

You ask Pi to run ll, and it reports ll: command not found. You try the same command in your interactive terminal, and it works. Pi ran the command in a separate non-interactive shell, where your alias may not be defined or expanded.

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

    flowchart TD
  YOU["your interactive terminal"] -->|"aliases, rc files, your PATH"| YSH["your shell"]
  PI["pi, started from somewhere"] --> SP["shellPath setting<br/>default: resolved bash"]
  SP --> FRESH["a fresh non-interactive shell<br/>per command"]
    FRESH --> NOALIAS["aliases not expanded by default<br/>interactive startup files not normally read"]
  FRESH --> BANG["your !command and !!command<br/>run here too"]
  EXT["an extension providing a shell tool"] -.->|"bypasses shellCommandPrefix"| OWN["its own implementation"]
  

The gap between the top box and the bottom is the whole chapter. Your terminal shell is interactive; Pi normally starts a fresh non-interactive process for each command. It may inherit environment variables from the Pi process, but not the interactive shell state you have accumulated since launching your terminal.

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 JSON escape for a newline; when Pi reads the setting, it becomes a newline between the two setup commands. Pi then appends the requested command after the prefix. Verify the configuration first from the editor:

!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 useful diagnostic rule is to inspect PI_PROVIDER and PI_MODEL from a model-invoked shell tool when you need to identify the model Pi selected. They do not necessarily reveal the upstream model chosen by a provider that routes requests.

Wrong: “Pi’s commands run in my terminal, so if it works for me it works for Pi.” Correct: Pi starts separate shell processes, and the model-invoked tools can receive session variables that user-entered ! and !! commands do not. Even when both paths resolve to the same Bash executable, their environments can differ.

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 make PowerShell the model-facing shell tool instead of Bash on native Windows, replace the selected tool in defaultTools:

{
  "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 is empty, the command may have run under a non-Bash shell; check which tool and executable actually handled it before drawing conclusions about your setup. Running the same command yourself with ! is not an equivalent test, because user-entered commands do not receive the same Pi session variables as model-invoked tool calls.

Once you have verified the executable, startup prefix, and injected variables, shell-related failures become easier to separate from task failures. The next step is to specify what the task should produce and how you will check it, rather than assuming that a correctly configured shell guarantees a correct result.

What was run

examples/ch09-shell/ drives the real bash tool and the real ! path in a session, with a scripted model asking for commands:

  • the five session variables are injected into the model’s bash tool: PI_SESSION_ID is the session’s id, PI_PROVIDER is the selected provider, and PI_SESSION_FILE is the JSONL path;
  • PI_SESSION_FILE is empty for an ephemeral session, which is how a script can tell;
  • the values are resolved when each command starts: after setThinkingLevel("high") the next command printed level=high with no restart;
  • session variables are not injected into a user-entered ! command, while the model’s command saw them. A check you run by hand sees a different environment from the one the model’s check sees;
  • shellCommandPrefix ran before both the model’s commands and ! commands;
  • an alias is absent in the non-interactive shell, and works once the prefix runs shopt -s expand_aliases and defines it;
  • a non-zero exit comes back as a result the model can read, with the exit code in it.

These observations came from a Windows run using Git Bash. Other platforms, different shellPath settings, and replacement shell tools can change startup behaviour; the tests do not establish identical results for every environment.