pi-guard
General-purpose permission system for pi tools, handling permissions for bash and file tools with extensible matchers for custom tools.
Package details
Install pi-guard from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-guard- Package
pi-guard- Version
1.5.0- Published
- Sep 25, 2026
- Downloads
- 568/mo · 315/wk
- Author
- jdiamond
- License
- MIT
- Types
- extension
- Size
- 105.7 KB
- Dependencies
- 2 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
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 - 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"}}'
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 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 |