@gotgenes/pi-permission-system
Permission enforcement extension for the Pi coding agent.
Package details
Install @gotgenes/pi-permission-system from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@gotgenes/pi-permission-system- Package
@gotgenes/pi-permission-system- Version
4.9.0- Published
- May 5, 2026
- Downloads
- 3,396/mo ยท 3,396/wk
- Author
- gotgenes
- License
- MIT
- Types
- extension
- Size
- 723.2 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
๐ @gotgenes/pi-permission-system
Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
Fork notice: This package is a full fork of MasuRii/pi-permission-system, published to npm as
@gotgenes/pi-permission-system. It has diverged substantially from upstream in config format, internal architecture, and permission model. The/permission-systemslash command name is the only upstream identity preserved.
Features
- Tool Filtering โ Hides disallowed tools from the agent before it starts (reduces "try another tool" behavior)
- System Prompt Sanitization โ Removes denied tool entries from the
Available tools:system prompt section so the agent only sees tools it can actually call - Runtime Enforcement โ Blocks/asks/allows at tool call time with UI confirmation dialogs and readable approval summaries
- Bash Command Control โ Wildcard pattern matching for granular bash command permissions
- MCP Access Control โ Server and tool-level permissions for MCP operations
- Skill Protection โ Controls which skills can be loaded or read from disk, including multi-block prompt sanitization
- Per-Agent Overrides โ Agent-specific permission policies via YAML frontmatter
- Subagent Permission Forwarding โ Forwards
askconfirmations from non-UI subagents back to the main interactive session - File-Based Review Logging โ Writes permission request/denial review entries to a file by default for later auditing
- Optional Debug Logging โ Keeps verbose extension diagnostics in a separate file when enabled in
config.json - JSON Schema Validation โ Full schema for editor autocomplete and config validation
- External Directory Guard โ Enforces
special.external_directoryfor path-bearing file tools and bash commands that reference paths outside the active working directory
Installation
npm package
pi install npm:pi-permission-system
Local extension folder
Place this folder in one of the following locations:
| Scope | Path |
|---|---|
| Global default | ~/.pi/agent/extensions/pi-permission-system (respects PI_CODING_AGENT_DIR) |
| Project | .pi/extensions/pi-permission-system |
Pi auto-discovers extensions in these paths.
Tip: All
~/.pi/agentpaths shown in this document are defaults. If thePI_CODING_AGENT_DIRenvironment variable is set, pi uses that directory instead. The extension automatically follows pi'sgetAgentDir()helper, so global policy files, per-agent overrides, session directories, and extension installation paths all resolve under the configured agent directory.
Usage
Quick Start
- Create the global config file (default:
~/.pi/agent/extensions/pi-permission-system/config.json, respectsPI_CODING_AGENT_DIR):
{
"permission": {
"*": "ask",
"read": "allow",
"write": "deny"
}
}
- Start Pi โ the extension automatically loads and enforces your policy.
Permission States
All permissions use one of three states:
| State | Behavior |
|---|---|
allow |
Permits the action silently |
deny |
Blocks the action with an error message |
ask |
Prompts the user for confirmation via UI |
Pi Integration Hooks
The extension integrates via Pi's lifecycle hooks:
| Hook | Behavior |
|---|---|
before_agent_start |
Filters active tools, removes denied tool entries from the system prompt, and hides denied skills |
tool_call |
Enforces permissions for every tool invocation |
input |
Intercepts /skill:<name> requests and enforces skill policy |
Additional behaviors:
- Unknown/unregistered tools are blocked before permission checks (prevents bypass attempts)
- The
Available tools:system prompt section is rewritten to match the filtered active tool set - Extension-provided tools like
task,mcp, and third-party tools are handled by exact registered name instead of private built-in hardcodes - When a subagent hits an
askpermission without direct UI access, the request can be forwarded to the main interactive session for confirmation - Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries instead of raw multiline JSON
- Permission review logs include bounded
toolInputPreviewvalues for non-bash/non-MCP tool calls so approvals can be audited without writing raw full payloads - Path-bearing file tools (
read,write,edit,find,grep,ls) evaluatepermission.external_directorybefore their normal tool permission when an explicit path points outsidectx.cwd - Bash commands are parsed with a full bash AST (
web-tree-sitter+tree-sitter-bash) to extract path-bearing arguments; only genuine command arguments and redirect destinations are checked โ heredoc bodies, comments, and quoted string contents are correctly excluded โ and paths that resolve outsidectx.cwdtrigger the samepermission.external_directorygate before the normal bash pattern check
Configuration
Config File
Location: one unified config file per scope, following the pi-autoformat convention:
| Scope | Path |
|---|---|
| Global | ~/.pi/agent/extensions/pi-permission-system/config.json (respects PI_CODING_AGENT_DIR) |
| Project | <cwd>/.pi/extensions/pi-permission-system/config.json |
Project config overrides global config; per-agent frontmatter overrides both.
The permission object uses deep-shallow merge: string-vs-string replaces; both-object shallow-merges pattern maps; string-vs-object the override wins entirely.
Scalar fields (debugLog, permissionReviewLog, yoloMode) use simple replacement.
The config file combines runtime knobs and permission policy in one object:
{
"$schema": "https://raw.githubusercontent.com/gotgenes/pi-permission-system/main/schemas/permissions.schema.json",
// Runtime knobs
"debugLog": false,
"permissionReviewLog": true,
"yoloMode": false,
"piInfrastructureReadPaths": [], // extra dirs to auto-allow for reads
// Flat permission policy
"permission": {
"*": "ask", // universal fallback
"read": "allow",
"write": "deny",
"bash": { "git status": "allow", "git *": "ask" },
"mcp": { "mcp_status": "allow" },
"skill": { "*": "ask" },
"external_directory": "ask"
}
}
Runtime knobs
| Key | Default | Description |
|---|---|---|
debugLog |
false |
Enables verbose diagnostic logging to logs/pi-permission-system-debug.jsonl |
permissionReviewLog |
true |
Enables the permission request/denial review log at logs/pi-permission-system-permission-review.jsonl |
yoloMode |
false |
Auto-approves ask results instead of prompting when yolo mode is enabled |
piInfrastructureReadPaths |
[] |
Extra directories to auto-allow for reads, bypassing the external_directory gate (supports ~) |
Both logs write to ~/.pi/agent/extensions/pi-permission-system/logs/.
No debug output is printed to the terminal.
Policy sections
The config file is a JSON object with these policy sections:
The permission object maps surface names to actions:
| Key | Value type | Description |
|---|---|---|
"*" |
string | Universal fallback โ applies when no surface-specific rule matches |
tool name (e.g. read) |
string | Catch-all for that tool surface |
bash |
string or object | Bash catch-all or { pattern: action } map |
mcp |
string or object | MCP catch-all or { pattern: action } map |
skill |
string or object | Skill catch-all or { pattern: action } map |
external_directory |
string or object | Controls access to paths outside cwd; supports ~/ and $HOME/ patterns |
Note: Trailing commas are not supported. If parsing fails, the extension falls back to
askfor all categories.
Global Per-Agent Overrides
Override global permissions for specific agents via YAML frontmatter in the global Pi agents directory (default: ~/.pi/agent/agents/<agent>.md, respects PI_CODING_AGENT_DIR):
---
name: my-agent
permission:
tools:
read: allow
write: deny
mcp: allow
bash:
git status: allow
git *: ask
mcp:
chrome_devtools_*: deny
exa_*: allow
skills:
"*": ask
---
MCP behavior: permission.tools.mcp is the coarse entry/fallback permission for a registered mcp tool when one is available. More specific permission.mcp target rules override that fallback when they match.
Limitations: The frontmatter parser is intentionally minimal. Use only key: value scalars and nested maps. Avoid arrays, multi-line scalars, and YAML anchors.
Project-Level Config and Overrides
Project-local config uses the same format as the global config file. Per-agent overrides use YAML frontmatter in the project agents directory:
| Scope | Path |
|---|---|
| Project config | <cwd>/.pi/extensions/pi-permission-system/config.json |
| Project agent override | <cwd>/.pi/agent/agents/<agent>.md |
These project files are resolved from Pi's current session cwd, so they are workspace-specific and do not move under PI_CODING_AGENT_DIR.
Precedence order:
- Global config file
- Project config file
- Global agent frontmatter
- Project agent frontmatter
Later layers override earlier layers. Within a surface map like bash or mcp, matching follows last matching rule wins โ put broad catch-alls first and specific overrides after. The recommended convention is also used by OpenCode's permission model.
Policy Reference
permission["*"] โ universal fallback
The "*" key sets the action used when no surface-specific rule matches:
{
"permission": {
"*": "ask"
}
}
Omitting "*" defaults to "ask" (least privilege).
Tool surfaces
Any registered tool name can be a surface key. A string value is a catch-all for that surface.
| Surface example | Description |
|---|---|
read, write, edit, grep, find, ls |
Canonical Pi built-in file tools |
bash |
Shell command execution |
mcp |
Registered MCP proxy tool |
task |
Delegation tool |
third_party_tool |
Any other registered extension tool |
{
"permission": {
"read": "allow",
"write": "deny",
"third_party_tool": "ask"
}
}
Unknown or absent tools are not required in the config. If a tool is not registered at runtime, this extension blocks it before permission checks run.
bash surface
Command patterns use * wildcards matched against the full command string. Last matching rule wins โ put broad catch-alls first, specific overrides after.
{
"permission": {
"bash": {
"*": "ask",
"git status": "allow",
"git diff": "allow",
"git *": "ask",
"rm -rf *": "deny"
}
}
}
String shorthand sets a catch-all for all bash commands:
{
"permission": { "bash": "allow" }
}
mcp surface
MCP permissions match against derived targets from tool input:
| Target type | Examples |
|---|---|
| Baseline ops | mcp_status, mcp_list, mcp_search, mcp_describe, mcp_connect |
| Server name | myServer |
| Server/tool combo | myServer:search, myServer_search |
| Generic | mcp_call |
{
"permission": {
"mcp": {
"*": "ask",
"mcp_status": "allow",
"mcp_list": "allow",
"myServer:*": "ask",
"dangerousServer": "deny"
}
}
}
Note: Baseline discovery targets auto-allow when any explicit
mcp: allowrule exists.
String shorthand grants broad MCP access โ useful for per-agent overrides:
# ~/.pi/agent/agents/researcher.md (respects PI_CODING_AGENT_DIR)
---
name: researcher
permission:
mcp: allow
---
skill surface
Skill name patterns use * wildcards (note: surface is skill, not skills):
{
"permission": {
"skill": {
"*": "ask",
"dangerous-*": "deny",
"librarian": "allow"
}
}
}
Home directory expansion in patterns
Pattern keys in any permission surface can start with ~/ or $HOME/ (or be exactly ~ / $HOME).
They are expanded to the OS home directory at match time, so configs are portable across machines and users.
{
"permission": {
"external_directory": {
"*": "ask",
"~/development/*": "allow"
}
}
}
The pattern is stored and displayed as written (e.g. ~/development/*) in logs and approval dialogs.
external_directory surface
Controls access to paths outside the active working directory. Use a pattern map to allow specific directories without opening all external access:
{
"permission": {
"external_directory": {
"*": "ask",
"~/development/*": "allow"
}
}
}
external_directory is evaluated before the normal tool permission check. For example, read: "allow" can permit ordinary reads while external_directory: "ask" still requires confirmation before reading ../outside.txt or an absolute path outside ctx.cwd.
Optional-path search tools (find, grep, ls) skip this check when no path is provided.
Bash commands are also covered: the extension extracts path-like tokens from the command string and applies the same gate when any resolve outside ctx.cwd.
Quoted strings are stripped first to reduce false positives.
This is a best-effort heuristic โ variable expansion, subshells, and escaped quotes are not parsed.
OS device paths (/dev/null, /dev/stdin, /dev/stdout, /dev/stderr) are always excluded.
Pi infrastructure read auto-allow โ Read-only tools (read, find, grep, ls) targeting Pi infrastructure directories are automatically allowed without triggering the gate, even when external_directory is ask or deny.
Infrastructure directories include:
- The agent config directory (
~/.pi/agent/or$PI_CODING_AGENT_DIR) - Git-cloned global packages (
<agentDir>/git/) - The global
node_modulesroot (auto-discovered from the extension's own install path โ works for npm, pnpm, bun, Homebrew) - Project-local Pi packages (
<cwd>/.pi/npm/and<cwd>/.pi/git/) - Any paths listed in
piInfrastructureReadPaths
Write tools (write, edit) to infrastructure paths are not auto-allowed and still go through the gate.
Common Recipes
Read-Only Mode
{
"permission": {
"*": "ask",
"read": "allow",
"grep": "allow",
"find": "allow",
"ls": "allow",
"write": "deny",
"edit": "deny"
}
}
Restricted Bash Surface
{
"permission": {
"*": "ask",
"bash": {
"*": "deny",
"git status": "allow",
"git diff": "allow",
"git log *": "allow"
}
}
}
MCP Discovery Only
{
"permission": {
"*": "ask",
"mcp": {
"*": "ask",
"mcp_status": "allow",
"mcp_list": "allow",
"mcp_search": "allow",
"mcp_describe": "allow"
}
}
}
Per-Agent Lockdown
In the global Pi agents directory (default: ~/.pi/agent/agents/reviewer.md, respects PI_CODING_AGENT_DIR):
---
permission:
write: deny
edit: deny
bash: deny
---
Technical Details
Permission Prompt Summaries
When a tool permission resolves to ask, the prompt is designed to be readable enough for an informed approval decision:
bashprompts show the command and matched bash pattern when available.mcpprompts show the derived MCP target and matched rule when available.- Built-in file tools show concise summaries, such as the target path and edit/write line counts, instead of raw multiline JSON.
- Unknown or third-party extension tools show a bounded single-line JSON preview of the input so users are not asked to approve a blind tool name.
Example edit approval prompt:
Current agent requested tool 'edit' for '.gitignore' (1 replacement: edit #1 replaces 5 lines with 2 lines). Allow this call?
Session-Scoped Approvals
When any permission resolves to ask, the permission dialog offers four options:
Yes | Yes, allow "<pattern>" for this session | No | No, provide reason
Selecting Yes, allow "<pattern>" for this session approves the current request and records the suggested wildcard pattern as a session rule. Subsequent requests that match the pattern skip the prompt for the remainder of the session.
The suggested pattern is surface-specific:
| Surface | Example request | Suggested session pattern |
|---|---|---|
| bash | git status --short |
git status * |
| mcp (qualified) | exa:search |
exa:* |
| mcp (munged) | exa_search |
exa_* |
| skill | librarian |
librarian |
| tool (read, write, โฆ) | read |
* |
| external_directory | /other/project/src/foo.ts |
/other/project/src/* |
Bash arity table
Bash pattern suggestions use a curated arity dictionary (src/bash-arity.ts) to determine how many tokens define the "human-understandable subcommand."
Longest matching prefix wins, so npm run (arity 3) takes precedence over npm (arity 2).
Unknown commands default to arity 1 (first word only).
| Example command | Arity entry matched | Suggested pattern |
|---|---|---|
git checkout main |
git โ 2 |
git checkout * |
npm run dev |
npm run โ 3 |
npm run dev* |
npm install lodash |
npm โ 2 |
npm install * |
docker compose up |
docker compose โ 3 |
docker compose up * |
rm -rf node_modules |
rm โ 1 |
rm * |
mytool --verbose |
(unknown) โ 1 | mytool * |
The arity table covers common CLI tools including git, npm/pnpm/yarn/bun, docker, cargo, go, kubectl, gh, and others.
To add an entry, open src/bash-arity.ts and add a key/arity pair to the ARITY object.
Put the most specific multi-word prefix first (e.g. "npm run": 3) before the shorter fallback ("npm": 2).
Session approvals are ephemeral โ they are never persisted to disk and are cleared on session_shutdown.
The review log records these decisions: resolution: "approved_for_session" when the user approves, and resolution: "session_approved" when a later request is matched by an existing session rule.
Subagent Permission Forwarding
When a delegated or routed subagent runs without direct UI access, ask permissions can still be enforced by forwarding the confirmation request through Pi session directories. The main interactive session polls for forwarded requests, shows the confirmation prompt, writes the response, and the subagent resumes once that decision is available.
This keeps ask policies usable even when the original permission check happens inside a non-UI execution context.
Logging
When the extension prompts, denies, or forwards permission requests, it can append structured JSONL entries under:
Default global logs directory: ~/.pi/agent/extensions/pi-permission-system/logs/
Actual global logs directory: $PI_CODING_AGENT_DIR/extensions/pi-permission-system/logs/ when PI_CODING_AGENT_DIR is set
pi-permission-system-permission-review.jsonlโ enabled by default for permission review/audit history, including boundedtoolInputPreviewvalues for non-bash/non-MCP tool callspi-permission-system-debug.jsonlโ disabled by default and intended for troubleshooting
On every session start, the extension emits a config.resolved entry to both logs listing the resolved config paths and whether each exists.
This makes it easy to verify which files the extension actually loaded:
{
"event": "config.resolved",
"globalConfigPath": "/โฆ/.pi/agent/extensions/pi-permission-system/config.json",
"globalConfigExists": true,
"projectConfigPath": "/โฆ/my-project/.pi/extensions/pi-permission-system/config.json",
"projectConfigExists": false,
"agentsDir": "/โฆ/.pi/agent/agents",
"agentsDirExists": true,
"projectAgentsDir": "/โฆ/my-project/.pi/agent/agents",
"projectAgentsDirExists": false,
"legacyGlobalPolicyDetected": false,
"legacyProjectPolicyDetected": false,
"legacyExtensionConfigDetected": false
}
Architecture
index.ts โ Root Pi entrypoint shim
src/
โโโ index.ts โ Extension bootstrap, permission checks, readable prompts, review logging, reload handling, and subagent forwarding
โโโ pattern-suggest.ts โ Per-surface session approval pattern suggestions
โโโ bash-arity.ts โ Curated arity dictionary for smarter bash session-approval patterns
โโโ session-rules.ts โ Ephemeral session-scoped approval rules (Ruleset-based, wildcard patterns across all surfaces)
โโโ config-loader.ts โ Unified config loader, merger, and legacy-path detection
โโโ config-paths.ts โ Path derivation for global, project, and legacy config locations
โโโ config-reporter.ts โ Resolved config path reporting for diagnostic logs
โโโ extension-config.ts โ Runtime config normalization and defaults
โโโ logging.ts โ File-only debug/review logging helpers
โโโ permission-manager.ts โ Global/project policy loading, merging, and resolution with caching
โโโ skill-prompt-sanitizer.ts โ Skill prompt parsing, multi-block sanitization, and skill-read path matching
โโโ bash-filter.ts โ Bash command wildcard pattern matching
โโโ wildcard-matcher.ts โ Shared wildcard pattern compilation and matching
โโโ common.ts โ Shared utilities (YAML parsing, type guards, etc.)
โโโ tool-registry.ts โ Registered tool name resolution
โโโ types.ts โ TypeScript type definitions
tests/
โโโ permission-system.test.ts โ Core permission, layering, forwarding, and policy tests
โโโ config-modal.test.ts โ Config command and modal behavior tests
โโโ test-harness.ts โ Shared lightweight test helpers
schemas/
โโโ permissions.schema.json โ JSON Schema for policy validation
config/
โโโ config.example.json โ Starter global policy template
Module Organization
The extension uses a modular architecture with shared utilities:
| Module | Purpose |
|---|---|
common.ts |
Shared utilities: toRecord(), getNonEmptyString(), isPermissionState(), parseSimpleYamlMap(), extractFrontmatter() |
wildcard-matcher.ts |
Compile-once wildcard patterns with last-match-wins evaluation: compileWildcardPatterns(), findCompiledWildcardMatch() |
permission-manager.ts |
Policy resolution with file stamp caching for performance |
bash-filter.ts |
Uses shared wildcard matcher for bash command patterns |
skill-prompt-sanitizer.ts |
Parses all available skill prompt blocks, removes denied skills, and tracks visible skill paths for read protection |
Performance Optimizations
- File stamp caching: Configurations are cached with file modification timestamps to avoid redundant reads
- Pre-compiled patterns: Wildcard patterns are compiled to regex once and reused across permission checks
- Resolved permissions caching: Merged agent+global permissions are cached per-agent with invalidation on file changes
Threat Model
Goal: Enforce policy at the host level, not the model level.
What this stops:
- Agent calling tools it shouldn't use (e.g.,
write, dangerousbash) - Tool switching attempts (calling non-existent tool names)
- Accidental escalation via skill loading
- Unapproved path-bearing tool access outside the active working directory when
external_directoryisaskordeny
Limitations:
- If a dangerous action is possible via an allowed tool, policy must explicitly restrict it
- This is a permission decision layer, not a sandbox
Schema Validation
Validate your config against the included schema:
npx --yes ajv-cli@5 validate \
-s ./schemas/permissions.schema.json \
-d ./pi-permissions.valid.json
Editor tip: Add "$schema": "./schemas/permissions.schema.json" to your config for autocomplete support.
Migration from pre-v2 layout
Before v2, config was split across two files:
- Policy:
~/.pi/agent/pi-permissions.jsonc - Runtime knobs:
<extension-install-dir>/config.json
These are now consolidated into one file. The extension detects legacy files and merges them with a warning for one release. To migrate manually:
# Move the global policy file
mkdir -p ~/.pi/agent/extensions/pi-permission-system
mv ~/.pi/agent/pi-permissions.jsonc ~/.pi/agent/extensions/pi-permission-system/config.json
# If you had project-level policy:
mkdir -p .pi/extensions/pi-permission-system
mv .pi/agent/pi-permissions.jsonc .pi/extensions/pi-permission-system/config.json
Then add any runtime knobs (debugLog, permissionReviewLog, yoloMode) to the same file.
The old extension-root config.json is no longer read from the install directory.
Note: Logs also moved from
<extension-install-dir>/logs/to~/.pi/agent/extensions/pi-permission-system/logs/. Old log files are not deleted or migrated โ they remain readable but no new entries are appended.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Config not applied (everything asks) | File not found or parse error | Verify the global config at ~/.pi/agent/extensions/pi-permission-system/config.json (respects PI_CODING_AGENT_DIR); check for trailing commas |
| Per-agent override not applied | Frontmatter parsing issue | Ensure --- delimiters at file top; keep YAML simple; restart session |
| Tool blocked as unregistered | Unknown tool name | Use a registered mcp tool for server tools: { "tool": "server:tool" } |
/skill:<name> blocked |
Deny policy or confirmation unavailable | Check merged skills policy (global/project/agent layers). Active agent context is optional in the main session; ask still requires UI or forwarded confirmation. |
| External file path blocked | special.external_directory is ask without UI or deny |
Allow/ask the special permission or keep file tools inside the active working directory. |
| Permission prompt is too verbose | Generic extension tool input is large | Built-in file tools are summarized automatically; third-party tools are capped to a bounded one-line JSON preview. |
Development
pnpm run build # Type-check TypeScript (no emit)
pnpm run lint # Biome lint + format check
pnpm run lint:fix # Biome lint + format auto-fix
pnpm run lint:md # markdownlint-cli2 on README etc.
pnpm run lint:all # lint + lint:md
pnpm run format # Biome format --write
pnpm run test # Run tests from ./tests
pnpm run check # build + lint:all + test
Pre-commit hooks
This project uses prek to run Biome and markdownlint on staged files before each commit. This catches lint and formatting issues locally instead of waiting for CI.
- Install prek (installation guide).
- Run
pnpm installโ thepreparescript callsprek installautomatically. If prek is not installed, the script prints a warning and continues. - Hooks run automatically on
git commit. To skip in emergencies:git commit --no-verify.
The hook configuration lives in prek.toml at the repo root.
Acknowledgments
This project began as a fork of MasuRii/pi-permission-system. Thank you to MasuRii for the original work that made this possible.
Thank you to the OpenCode team for the permission model design that inspired the flat config format and evaluation semantics used in this extension.