pi-guard1.6.0
General-purpose permission system for pi tools, handling permissions for bash and file tools with extensible matchers for custom tools.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
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 - Control-flow-aware prompts — Approval prompts preserve loop, conditional, subshell, and brace-group structure while showing each command's status
- 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 — Temporarily allow commands or tools 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
Temporarily allow rm (this session only)
Permanently allow rm (save to settings.json)
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
Temporarily allow curl (this session only)
Permanently allow curl (save to settings.json)
Reject
Shell control flow remains visible in the prompt. For example, when the agent runs:
for stack in staging prod; do
if [ -f "infra/$stack/config" ]; then
curl -fsS "https://$stack.example.com/health"
fi
done
The prompt shows the conditions and loop around each checked command:
⚠️ Unapproved Commands
for stack in staging prod; do
if
✔ [ -f "infra/$stack/config" ]
then
✖ curl -fsS "https://$stack.example.com/health"
fi
done
→ Allow
Temporarily allow curl (this session only)
Permanently allow curl (save to settings.json)
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
Temporarily allow sed (this session only)
Permanently allow sed (save to settings.json)
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
Temporarily allow rm (this session only)
Permanently allow rm (save to settings.json)
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
Temporarily allow write (this session only)
Permanently allow write (save to settings.json)
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
Temporarily allow web_fetch (this session only)
Permanently allow web_fetch (save to settings.json)
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"}}'
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=strict
The profile name must exist in the loaded profiles configuration. Deactivating
the profile clears the propagated value for this process and its child sessions.
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 and process substitutions, heredocs,
if/elif/while/until/for/case, brace groups, and 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, [, test, etc.) and loop-control builtins (break, continue) 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 |