@inobit/pi-permission
Lightweight permission enforcement extension for the Pi coding agent: sensitive-file guard, read/write-aware project boundary, dangerous-operation confirmation, and plan/build read-only mode.
Package details
Install @inobit/pi-permission from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@inobit/pi-permission- Package
@inobit/pi-permission- Version
1.0.1- Published
- Aug 23, 2026
- Downloads
- 2,124/mo · 2,124/wk
- Author
- inobit
- License
- MIT
- Types
- extension
- Size
- 135.3 KB
- Dependencies
- 0 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
@inobit/pi-permission
English | 中文
Lightweight permission control for Pi coding agent. Decisions are tiered by "effect provability" — only block what needs blocking:
- Sensitive file protection:
.env,~/.ssh/*,*.pem,.npmrc,~/.config/gh/hosts.ymletc. — any read or write (tool or bash) triggers a confirmation prompt. - Trust domain boundary: plan is read-only (out-of-domain writes silently denied); build trust domain = project dir ∪ trusted paths — everything inside is allowed, crossings are gated.
- Dangerous operation confirmation:
git push/rm -rf,sudo,curl | shand the danger overlay always require confirmation; unverifiable execution (interpreters/unknown programs) asks in plan and is allowed inside the build domain. - Plan / Build modes:
/planis read-only (out-of-domain writes silently denied),/buildreturns to normal.
Effect Classification Model (R/W/X)
Commands are classified by "can side effects be fully derived from the arguments" into three tiers, plus a danger overlay:
| Tier | Criterion | Examples |
|---|---|---|
| R pure reader | No file side effects | cat/grep/jq/git status/sort (no -o) |
| W bounded writer | Write targets fully enumerable from arguments | touch/mkdir/cp/mv/sed -i/redirects/find -delete |
| X opaque | Effects not derivable — interpreters, build tools, every unrecognized program, parse-failure downgrades | python3/npm/make/bash -c/tar extraction/patch |
The danger overlay (rm -r/-f, chmod/chown -R, git write subcommands, curl|sh, sudo, bash -c, xargs, find -exec) sits above the tiers and keeps the existing product contract.
Features
- Zero third-party dependencies (besides Pi core packages), purely local deterministic decisions, fail-closed — never silently allows
- Precise read/write distinction down to path granularity; symlinks and relative paths cannot bypass, relative paths after
cdare resolved correctly - Sensitive file rules apply uniformly across all tools + bash
- Trust domain model: plan domain = trusted paths (e.g.
/tmp, free scratch reads/writes); build domain = project ∪ trusted — everything inside is allowed including executors; dangerous/sensitive checks always take precedence - Zero friction for high-frequency essentials (
cat/grep/ls/git status/sleepetc.)
Installation
pi install npm:@inobit/pi-permission
Commands
/plan enter read-only planning mode (writes denied, status bar shows Plan)
/build return to normal mode (Build)
/yolo enter yolo mode (allows everything except sensitive files, requires second confirmation, status bar shows Yolo)
/readonly-tools manage read-only tools for plan mode (multi-select with Space, session/project/global scopes)
- Defaults to build mode, session-scoped and non-persistent (resets to build on restart).
yolorequires a second confirmationy: confirm yolowith no shortcut. - Toggle shortcut:
Alt+Pcycles between plan/build (remappable viatoggleModeShortcut, empty string to disable; key format follows Pi keybindings) - Status bar:
Plangreen /Buildred (theme-aware), status keypi-permission-mode
Core Decision Paths
Plan mode (read-only contract)
danger overlay hit (rm -rf / git push / sudo / curl|sh ...) -> silent deny
enumerable write target outside trust domain T_plan -> silent deny
(includes project files; plain args of X segments are refs only)
sensitive file involved (read or write) -> ask
all segments are R or W (W targets proven inside T_plan) -> allow
fallback: any X segment (unverifiable effects) -> strict ? silent deny : ask
Build mode
danger overlay hit -> ask
sensitive file involved (read or write) -> ask
all refs & write targets of every segment inside T_build -> allow (R/W/X alike)
(T_build = cwd + trusted paths; python3 /tmp/x.py and npm test both allowed)
pure R (refs anywhere) -> allow
otherwise (W/X with cross-domain refs) -> ask
Pre-layer: unparseable syntax / $() / subshell / process substitution -> fail-closed before the tables (build=ask, plan=deny). Yolo mode skips all checks except sensitive files (still denied). Ask dialogs pick s to approve per program/path/parent-dir for the session; hints show once per rule per session.
Threat Model & Residual Risks (must read)
- Allowing executors inside the build trust domain = accepting the full capability of arbitrary code execution there. Script content can silently cross trust domains (write
~/.ssh, network) — the permission system sees the command surface, not code behavior. This extension is an in-process rule layer, not a sandbox; use containers/disposable environments for real isolation - W-tier safety equals target-enumeration correctness; unparseable syntax conservatively downgrades to X, but enumeration bugs themselves become mis-allows
- TOCTOU/symlink races are mitigated, not eliminated;
/tmpis world-writable on multi-user machines - Plan mode has no executor exemption mechanism: X segments always ask (silent deny under strict), keeping the read-only contract intact
Configuration
Merged by layer (array fields are union-deduplicated across layers, non-array fields are overridden by higher layers):
| Layer | Location |
|---|---|
| Global | ~/.pi/agent/extensions/pi-permission/config.json |
| Project | .pi/extensions/pi-permission/config.json (project must be trusted) |
| Session | In-memory (via /readonly-tools with session scope, lost on restart) |
| Field | Description | Default |
|---|---|---|
sensitivePatterns |
Sensitive file glob list | *.env *.env.* ~/.ssh/* *.pem *.key id_rsa* credentials.json secrets*.yaml ~/.aws/* .npmrc ~/.config/gh/hosts.yml |
envExampleReadAllowed |
Allow reading .env.example without prompt |
true |
readonlyBashCommands |
Bash read allowlist | High-frequency read-only commands (cat/grep/ls/..., 72 entries) |
dangerousBashCommands |
Unified dangerous operation list (sudo or git commit) |
Git write subcommands + dangerous shell |
trustedExternalPaths |
Trusted external path prefixes — reads/writes under these prefixes are auto-allowed (e.g. /tmp for temp files; os.tmpdir() is merged at runtime) |
["/tmp"] |
readonlyTools |
Tool read allowlist (union across layers) | read grep find ls |
strictPlanMode |
Plan mode: unverifiable execution (X segments) tightened from ask to silent deny | false |
toggleModeShortcut |
Plan/build toggle shortcut (empty string to disable) | alt+p |
reviewLog |
Review log toggle (FR-6) | true |
debugLog |
Debug log toggle (separate from review log, verbose events) | false |
logDir |
Log directory (relative to ~/.pi/agent, respects PI_CODING_AGENT_DIR; supports absolute path and ~/, 0600; extension dir holds only config) |
logs/pi-permission |
Fixed rules (not configurable): built-in write tools
write/edit,rm -r/-f,chmod -R,chown -R,curl/wget | sh/bash,bash -c/eval/sudo/xargs/find -execare always treated as dangerous; redirect targets>/>>are always checked; git subcommands not indangerousBashCommandsare treated as read-only. Prompt reasons carry a[bash]/[tool:<name>]source prefix and include configuration hints.Log location: defaults to
~/.pi/agent/logs/pi-permission/<project>/pi-permission-{review,debug}.jsonl(co-located withpi-debug.log), isolated per project, files0600, dirs0700, with size-based rotation. The extension directory~/.pi/agent/extensions/pi-permissionholds onlyconfig.json. Custom paths support absolute and~/forms, e.g."logDir": "~/my-logs/pi-permission"or"/var/log/pi-permission".Trusted exemption boundary: only the "directory boundary" is exempted, always after dangerous/sensitive checks — even inside a trusted directory, writes matching a sensitive filename (e.g.
/tmp/.env, or writing.envwhile cwd is/tmp) are still deny in plan and ask in build; dangerous commands (sudo rm -rf /tmpetc.) are path-independent and always intercepted before trusted checks; dual-form realpath guards against symlink escapes.
Ask Dialog
ask (= confirmation required) shows a 4-option selector + optional emacs input. All denies are terminate:false so the model immediately sees the reason and continues; only Esc hard terminate forces true.
| Key | Action | terminate | Audit |
|---|---|---|---|
y |
allow once | — | allow-after-ask |
s |
allow session (remembered as <session>:<approvalKey>) |
— | allow-after-ask + sessionApprovals |
n |
deny (default) | false (model continues) |
deny |
r |
deny with reason → emacs input | false (fully replaces denyFeedback text, model continues) |
deny + customReason (truncated, redacted) |
Esc (on select) |
hard terminate — deny and stop | true (force) |
deny + terminatedByEsc (reason="[pi-permission] Denied by user — stopping.") |
r second layer (ctx.ui.input, inherits tui.input.* emacs keys C-a/e/k/u/f/b): title Deny reason — emacs keys, Enter submit, Esc to go back, placeholder e.g. use .env.example instead. Empty (trim()==="") notifies reason cannot be empty and stays; Esc (input===undefined) returns to the 4-option select; Enter non-empty returns {kind:"reason", customReason} fully replacing the default reason as [pi-permission] User denied: <custom>. No UI (rpc/print) degrades to notify + deny with default.
ask → select [y/s/n/r]
├─ y/s → allow (+ s remembered)
├─ n → deny (default, terminate:false → model continues)
├─ r → input (emacs)
│ ├─ non-empty Enter → deny with reason (fully replaces, terminate:false)
│ ├─ empty Enter → stay (notify)
│ └─ Esc → back to select
└─ Esc → deny + hard terminate (force true)
/readonly-tools Interaction
Space to select/deselect, ↑/↓/j/k to move, Enter to confirm, Esc/q to cancel. Pick the edit target first (each layer edits only itself, other layers are locked):
- session (in-memory, session-scoped): built-in + global + project-configured tools are locked
- project (writes to
.pi/extensions/pi-permission/config.json): built-in + global-configured are locked, project must be trusted - global (writes to global config.json): only built-in tools are locked
Built-in tools (read/grep/find/ls), bash, and write/edit are always locked.
Status Bar Integration
- Pi built-in statusline:
Plan/Buildappears in the footer extension status row, no configuration needed. - pi-powerline-footer: status value without
[prefix, goes into theextension_statusesaggregated segment; to place it at the leftmost of the main bar:
{
"powerline": {
"preset": "default",
"placement": "below",
"customItems": [
{ "id": "pi-mode", "statusKey": "pi-permission-mode", "position": "left", "excludeFromExtensionStatuses": true }
],
"layout": { "left": ["custom:pi-mode", "model", "thinking", "shell_mode", "path", "git", "queue", "context_pct", "cache_read", "cost"] }
}
}
Development
pnpm --filter @inobit/pi-permission check # tsc --noEmit
pnpm --filter @inobit/pi-permission test # vitest
pnpm --filter @inobit/pi-permission pack:check
pi -ne -e ./packages/pi-permission
License
MIT