Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
- 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
- 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
- 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
Place this folder in one of the following locations:
| Scope | Path |
|---|---|
| Global | ~/.pi/agent/extensions/pi-permission-system |
| 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.
- Create the global policy file at
~/.pi/agent/pi-permissions.jsonc:
- Start Pi — the extension automatically loads and enforces your policy.
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 |
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 - When a subagent triggers an
askpermission without UI access, the request can be forwarded to the main session and answered there
Location: ~/.pi/agent/extensions/pi-permission-system/config.json
The extension creates this file automatically when it is missing. It controls only extension-local logging behavior:
{
"debugLog": false,
"permissionReviewLog": true
}| 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 |
Both logs write to files only under the extension directory. No debug output is printed to the terminal.
Location: ~/.pi/agent/pi-permissions.jsonc
The policy file is a JSON object with these sections:
| Section | Description |
|---|---|
defaultPolicy |
Fallback permissions per category |
tools |
Exact-name tool permissions for registered tools |
bash |
Command pattern permissions |
mcp |
MCP server/tool permissions for calls routed through a registered mcp tool |
skills |
Skill name pattern permissions |
special |
Reserved permission checks |
Note: Trailing commas are not supported. If parsing fails, the extension falls back to
askfor all categories.
Override global permissions for specific agents via YAML frontmatter in ~/.pi/agent/agents/<agent>.md:
---
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
---Precedence: Agent frontmatter overrides global config (shallow-merged per section).
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.
Sets fallback permissions when no specific rule matches:
{
"defaultPolicy": {
"tools": "ask",
"bash": "ask",
"mcp": "ask",
"skills": "ask",
"special": "ask"
}
}Controls tools by exact registered name (no wildcards). This is the recommended standalone format for all tool entries, including Pi built-ins and arbitrary third-party extension tools.
| Tool name example | Description |
|---|---|
bash |
Shell command execution (tool-level fallback before bash pattern rules) |
read / write |
Canonical Pi built-in file tools |
mcp |
Registered MCP proxy tool entry/fallback when available |
task |
Delegation tool handled like any other registered extension tool |
third_party_tool |
Arbitrary registered extension tool |
{
"tools": {
"read": "allow",
"write": "deny",
"mcp": "allow",
"third_party_tool": "ask"
}
}Unknown or absent tools are not required in the config. If another extension is not installed, its tool simply will not be registered at runtime, and this extension will block attempts to call that missing tool before permission checks run.
Note: Setting
tools.bashaffects the default for bash commands, butbashpatterns can provide command-level overrides.Note: Setting
tools.mcpcontrols coarse access to a registeredmcptool when one is available. Specificmcprules still override it when a target pattern matches.Note: Top-level shorthand is only supported for the canonical Pi built-ins (
bash,read,write,edit,grep,find,ls) in agent frontmatter. Usepermission.tools.<name>formcp,task, and any third-party tool.
Command patterns use * wildcards and match against the full command string. If multiple patterns match, the last matching rule wins.
{
"bash": {
"git *": "ask",
"git status": "allow",
"rm -rf *": "deny"
}
}MCP permissions match against derived targets from tool input. These rules are more specific than tools.mcp and override that fallback when a pattern matches:
| 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 |
{
"mcp": {
"mcp_status": "allow",
"mcp_list": "allow",
"myServer:*": "ask",
"dangerousServer": "deny"
}
}Note: Baseline discovery targets may auto-allow when you permit any MCP rule.
A registered mcp tool can use tools.mcp as an entry permission point. This provides a fallback when no specific MCP pattern matches:
{
"tools": {
"mcp": "allow"
}
}This is useful for per-agent configurations where you want to grant MCP access broadly:
# In ~/.pi/agent/agents/researcher.md
---
name: researcher
permission:
tools:
mcp: allow
---The permission resolution order for MCP operations:
- Specific
mcppatterns (e.g.,myServer:toolName,myServer_*) tools.mcpfallback (if set)defaultPolicy.mcp
Skill name patterns use * wildcards:
{
"skills": {
"*": "ask",
"dangerous-*": "deny"
}
}Reserved permission checks:
| Key | Description |
|---|---|
doom_loop |
Controls doom loop detection behavior |
external_directory |
Controls access outside working directory |
tool_call_limit |
(schema only, not enforced yet) |
{
"special": {
"doom_loop": "deny",
"external_directory": "ask"
}
}{
"defaultPolicy": { "tools": "ask", "bash": "ask", "mcp": "ask", "skills": "ask", "special": "ask" },
"tools": {
"read": "allow",
"grep": "allow",
"find": "allow",
"ls": "allow",
"write": "deny",
"edit": "deny"
}
}{
"defaultPolicy": { "tools": "ask", "bash": "deny", "mcp": "ask", "skills": "ask", "special": "ask" },
"bash": {
"git status": "allow",
"git diff": "allow",
"git log *": "allow",
"git *": "ask"
}
}{
"defaultPolicy": { "tools": "ask", "bash": "ask", "mcp": "ask", "skills": "ask", "special": "ask" },
"mcp": {
"mcp_status": "allow",
"mcp_list": "allow",
"mcp_search": "allow",
"mcp_describe": "allow",
"*": "ask"
}
}In ~/.pi/agent/agents/reviewer.md:
---
permission:
tools:
write: deny
edit: deny
bash:
"*": deny
---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.
When the extension prompts, denies, or forwards permission requests, it can append structured JSONL entries under:
~/.pi/agent/extensions/pi-permission-system/logs/
pi-permission-system-permission-review.jsonl— enabled by default for permission review/audit historypi-permission-system-debug.jsonl— disabled by default and intended for troubleshooting
index.ts → Root Pi entrypoint shim
src/
├── index.ts → Extension bootstrap, permission checks, review logging, and subagent forwarding
├── extension-config.ts → Extension-local config loading and default creation
├── logging.ts → File-only debug/review logging helpers
├── permission-manager.ts → Policy loading, merging, and resolution with caching
├── 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
└── test.ts → Test runner
schemas/
└── permissions.schema.json → JSON Schema for policy validation
config/
└── config.example.json → Starter global policy template
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 specificity sorting: compileWildcardPatterns(), findCompiledWildcardMatch() |
permission-manager.ts |
Policy resolution with file stamp caching for performance |
bash-filter.ts |
Uses shared wildcard matcher for bash command patterns |
- 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
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
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
Validate your config against the included schema:
npx --yes ajv-cli@5 validate \
-s ./schemas/permissions.schema.json \
-d ./pi-permissions.valid.jsonEditor tip: Add "$schema": "./schemas/permissions.schema.json" to your config for autocomplete support.
| Problem | Cause | Solution |
|---|---|---|
| Config not applied (everything asks) | File not found or parse error | Verify file at ~/.pi/agent/pi-permissions.jsonc; 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 |
Missing context or deny policy | Requires active agent context; ask behaves as block in headless mode |
npm run build # Compile TypeScript
npm run lint # Run linter (uses build)
npm run test # Run tests
npm run check # Run lint + test- pi-multi-auth — Multi-provider credential management and quota-aware rotation
- pi-tool-display — Compact tool rendering and diff visualization
- pi-rtk-optimizer — RTK command rewriting and output compaction
- pi-MUST-have-extension — RFC 2119 keyword normalization for prompts
{ "defaultPolicy": { "tools": "ask", "bash": "ask", "mcp": "ask", "skills": "ask", "special": "ask" }, "tools": { "read": "allow", "write": "deny" } }