Deny-by-default tool interception for Pi agent sessions. Intercepts all tool calls (bash, read, write, edit, etc.) and enforces per-project allow/deny rules with persistent decision logging.
pi install npm:@randomm/pi-permissions
Create .pi/permissions.json in your project root:
{
"default": "deny",
"bash": {
"*": "deny",
"npm install *": "allow",
"git diff *": "allow",
"git push *": "deny"
},
"tools": {
"read": "allow",
"write": "deny",
"edit": "deny"
}
}default:"allow"or"deny"— fallback when no rule matchesbash: Pattern rules for bash tool calls.*matches everythingtools: Exact tool name rules (e.g.,"read": "allow")
- On
session_start, the plugin loads.pi/permissions.jsonand caches it - On every
tool_call, it checks:- Cached user decisions (from previous sessions) — highest priority
- Bash pattern rules in
config.bash(wildcard matching) - Tool-specific rules in
config.tools(exact match) config.defaultfallback
- If no rule matches a cached decision, the user is prompted via
ctx.ui.confirm() - On approve: decision is saved to
.pi/permissions.json— future calls use the cached decision - On deny: decision is saved persistently — the agent is never asked again for this exact call
User decisions are stored in the same file under _decisions:
{
"_decisions": {
"bash:npm install lodash": { "allowed": true, "timestamp": "2026-04-23T10:00:00Z" }
},
"bash": { ... },
"tools": { ... }
}Patterns use simple glob-style matching where * matches everything including spaces:
npm install *matchesnpm install lodashbut notnpm uninstall lodashgit diff *matchesgit diff --stagedbut notgit push origin*matches any input
- Atomic writes via temp file + rename
- Corruption handling with
.corruptedbackup - Deny decisions are persistent across sessions
Apache License 2.0 — see LICENSE for details.