原始内容
pi-guard
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.
Features
- 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
Examples
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/data
The 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.
Install
pi install npm:pi-guard
Configuration
Configure 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"
}
}
}
}
Shorthand
Disable all checks:
{ "guard": { "enabled": false } }
Whole-tool action (no pattern matching needed):
{ "guard": { "rules": { "write": "allow" } } }
Environment variable
Set PI_GUARD to inject rules from outside (e.g., by pi-spawn or CI/CD):
PI_GUARD='{"*":"deny","bash":{"git diff":"allow"}}'
Matchers
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.
Bash matching
- 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"matchesgit log,git log --oneline, andgit log --oneline -10. This means you can allow a command without enumerating every flag combination.
Wildcard tokens in bash rules
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).
Glob matching
Standard glob patterns:
*matches anything except/**matches anything including/?matches a single character~expands to home directory
Exact matching
Simple string equality. Rule "build" only matches input build.
Actions
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 |
Rule precedence
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"
}
Default rules
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
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.
Shortcuts
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.
Commands
| 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 |