@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.

Packages

Package details

extension

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:

  1. Sensitive file protection: .env, ~/.ssh/*, *.pem, .npmrc, ~/.config/gh/hosts.yml etc. — any read or write (tool or bash) triggers a confirmation prompt.
  2. 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.
  3. Dangerous operation confirmation: git push / rm -rf, sudo, curl | sh and the danger overlay always require confirmation; unverifiable execution (interpreters/unknown programs) asks in plan and is allowed inside the build domain.
  4. Plan / Build modes: /plan is read-only (out-of-domain writes silently denied), /build returns 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 cd are 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 / sleep etc.)

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). yolo requires a second confirmation y: confirm yolo with no shortcut.
  • Toggle shortcut: Alt+P cycles between plan/build (remappable via toggleModeShortcut, empty string to disable; key format follows Pi keybindings)
  • Status bar: Plan green / Build red (theme-aware), status key pi-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; /tmp is 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 -exec are always treated as dangerous; redirect targets >/>> are always checked; git subcommands not in dangerousBashCommands are 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 with pi-debug.log), isolated per project, files 0600, dirs 0700, with size-based rotation. The extension directory ~/.pi/agent/extensions/pi-permission holds only config.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 .env while cwd is /tmp) are still deny in plan and ask in build; dangerous commands (sudo rm -rf /tmp etc.) 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/Build appears in the footer extension status row, no configuration needed.
  • pi-powerline-footer: status value without [ prefix, goes into the extension_statuses aggregated 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