Permission system for pi tools
pi-guard intercepts tool calls and prompts for approval before executing potentially dangerous operations. It provides fine-grained, pattern-based permissions for bash commands, file access, and any custom tool — with sensible defaults that let you start safely.
- Bash command matching — Parses shell commands with an AST parser, handles pipes, subshells, wrapper commands (
sudo,xargs,bash -c,find -exec), and supports glob tokens in rules - Path matching — Glob patterns for file read/write/edit permissions
- Extensible — Add matchers for any tool with
exact,glob, orbashmatching - Sensible defaults — Reading is safe, writing is dangerous. Works out of the box
- Layered configuration — Default → user → project → env → profile → session, last match wins
- Non-interactive support — Denied commands are silently blocked in CI/CD; use
PI_GUARDenv var for automation - Session rules — "Always allow for this session" without modifying config files
When a tool call is covered by an ask rule, pi-guard intercepts it and prompts for approval. Commands get reformatted and abridged to make them easier to review. Allowed commands get ✔, unauthorized ones get ✖.
When the agent runs:
rm -rf dist/The prompt looks like:
⚠️ Unapproved Commands
✖ rm -rf dist/
→ Allow
Always allow rm (this session)
Reject
For commands with pipes and subshells, each sub-command is checked independently. When the agent runs:
TOKEN=$(curl -s https://api.example.com/token | jq -r .access_token) && \
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/dataThe prompt looks like:
⚠️ Unapproved Commands
✔ TOKEN=$(...) &&
✖ curl -s https://api.example.com/token |
✔ jq -r .access_token
✖ curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data
→ Allow
Always allow curl (this session)
Reject
Wrapper commands (xargs, find -exec) are expanded — the wrapper gets ✔, the inner command is checked on its own line. When the agent runs:
grep -rl 'TODO' src/ | xargs sed --in-place 's/TODO/DONE/g'The prompt looks like:
⚠️ Unapproved Commands
✔ grep -rl 'TODO' src/ |
✔ xargs ...
✖ sed --in-place s/TODO/DONE/g
→ Allow
Always allow sed (this session)
Reject
Or with find -exec:
find src/ -name '*.test.ts' -exec rm {} \;The prompt looks like:
⚠️ Unapproved Commands
✔ find src/ -name *.test.ts -exec ...
✖ rm {}
→ Allow
Always allow rm (this session)
Reject
For file operations, there's no command to parse — the prompt shows the path being accessed:
⚠️ Write Permission Required
src/lib/config.ts
→ Allow
Always allow write (this session)
Reject
For custom tools, the prompt shows the matched parameter value:
⚠️ web_fetch Permission Required
https://api.github.com/repos/jdiamond/pi-guard/issues
→ Allow
Always allow web_fetch (this session)
Reject
In non-interactive mode (e.g., CI), unauthorized commands are silently blocked without a prompt.
pi install npm:pi-guardConfigure in ~/.pi/agent/settings.json or .pi/settings.json (project-level):
{
"guard": {
"enabled": true,
"matchers": {
"spawn": { "param": "agent", "type": "exact" },
"webfetch": { "param": "url", "type": "glob" }
},
"rules": {
"*": "ask",
"bash": {
"*": "ask",
"git status": "allow",
"git log": "allow",
"rm": "deny"
},
"read": {
"*": "allow",
"**/*.env": "deny",
"**/*.pem": "deny"
},
"write": { "*": "ask" },
"edit": { "*": "ask" },
"spawn": {
"build": "allow",
"test": "allow",
"*": "deny"
},
"webfetch": {
"*": "ask",
"https://github.com/*": "allow"
}
}
}
}Disable all checks:
{ "guard": { "enabled": false } }Whole-tool action (no pattern matching needed):
{ "guard": { "rules": { "write": "allow" } } }Set PI_GUARD to inject rules from outside (e.g., by pi-spawn or CI/CD):
PI_GUARD='{"*":"deny","bash":{"git diff":"allow"}}'When a named profile is active, pi-guard also publishes its name in
PI_GUARD_PROFILE so child Pi sessions can activate the same profile:
PI_GUARD_PROFILE=strictThe profile name must exist in the loaded profiles configuration. Deactivating
the profile clears the propagated value for this process and its child sessions.
Matchers define how to extract and match input from a tool call. Each matcher has a param (which tool parameter to extract) and a type (how to match).
| Type | Description | Use case |
|---|---|---|
bash |
Parse command, extract all commands, subsequence match | Bash commands |
glob |
* and ** matching (paths, URLs) |
File paths, URLs |
exact |
String equality | Enum values, agent names |
Tools without a matcher get simple allow/ask/deny for the whole tool.
- Parse the command with the unbash AST parser
- Extract all commands from the AST (handles pipes, subshells, command substitutions, process substitutions, heredocs,
if/while/for/case, functions) - Expand wrapper commands (
xargs rm→xargs+rm,sudo rm→sudo+rm,bash -c 'rm -rf /'→bash -c+rm,find -exec rm {} \;→find -exec+rm) - For each command, check rules using subsequence matching — rule tokens must appear in order, extra arguments are allowed
Tip
"git log" matches git log, git log --oneline, and git log --oneline -10. This means you can allow a command without enumerating every flag combination.
Tokens containing * or ? are matched as globs against the corresponding command argument:
"sed": "allow",
"sed -i*": "ask",
"sed --in-place*": "ask"| Command | Result | Reason |
|---|---|---|
sed -E 's/old/new/' |
allow | sed rule, no -i flag |
sed -i 's/old/new/' |
ask | -i matches glob -i* |
sed -i.bak 's/old/new/' |
ask | -i.bak matches glob -i* |
This only applies to */? inside rule tokens. The bare "*" key is the catch-all for any command (see Rule precedence).
Standard glob patterns:
*matches anything except/**matches anything including/?matches a single character~expands to home directory
Simple string equality. Rule "build" only matches input build.
Each permission rule resolves to one of:
| Action | Behavior |
|---|---|
allow |
Run without approval |
ask |
Prompt for approval (block in non-interactive mode) |
deny |
Block the action |
default → user config → project config → env (PI_GUARD) → profile → session rules
Last match wins within a tool's rules. Put the catch-all "*" first, specific rules after:
"bash": {
"*": "ask",
"git status": "allow",
"git log": "allow",
"rm": "deny"
}See src/defaults.ts for the built-in defaults.
The defaults follow a simple principle: reading is safe, writing is dangerous. Read-only bash commands (ls, cat, git log, grep, etc.) are allowed, while anything that modifies state asks for approval. Note that sed is allowed by default, but sed -i* (in-place edit) is set to ask since it modifies files. File reads are mostly allowed except for sensitive patterns (*.env, *.pem). All edits and writes require approval.
Tip
To trust the agent with file modifications (useful in containers or trusted environments):
{
"guard": {
"rules": { "edit": "allow", "write": "allow" }
}
}Profiles let you define named rule overlays and switch between them during a session. Only one profile can be active at a time.
{
"guard": {
"profiles": {
"read-write": {
"edit": { "*": "allow" },
"write": { "*": "allow" }
}
}
}
}Activate with /guard profile read-write, deactivate with /guard profile off.
Warning
Profiles are layered between env and session rules. A profile with "*": "allow" will override specific rules from earlier layers (like "rm": "deny") because "*" always matches last and wins.
Define custom slash commands for quick access to guard actions:
{
"guard": {
"profiles": {
"read-write": {
"edit": { "*": "allow" },
"write": { "*": "allow" }
}
},
"shortcuts": {
"rw": "profile read-write",
"ro": "profile off",
"yolo": "disable",
"safe": "enable"
}
}
}Now /rw activates the read-write profile, /ro deactivates it, and /yolo//safe quickly toggle the guard.
Shortcuts can reference any guard subcommand: profile, list, toggle, enable, or disable.
| Command | Description |
|---|---|
/guard enable |
Enable guard |
/guard disable |
Disable guard |
/guard toggle |
Toggle guard on/off |
/guard list |
Show current rules by provenance layer |
/guard profile |
Show active profile and available profiles |
/guard profile <name> |
Activate a profile |
/guard profile off |
Deactivate current profile |