Skip to content
Agentic Control Plane

pi-permission-system and Pi permissions: the reference

David Crowe David Crowe · · Updated · 14 min read
pi permissions reference
Share X HN LinkedIn

Pi has no built-in permission system. Every permission you see in Pi comes from an extension: the permission-gate.ts example in Pi’s repo, the community pi-permission-system package, or your own handler on the tool_call event. The quickest way to get allow, ask and deny rules is pi install npm:pi-permission-system plus a pi-permissions.jsonc file.

This page covers all three: what Pi ships, the hook that permissions attach to, and how pi-permission-system decides. For our side of it, see the pi controls page.

What Pi ships: no permission system, on purpose

Pi’s README says it directly: “Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access.” It runs with the permissions of whoever launched it. The built-in tools are read, write, edit and bash.

For a hard boundary, the README points to containment instead of prompts:

  • Gondolin: an extension that keeps pi and provider auth on the host and routes built-in tools and ! commands into a local Linux micro-VM.
  • Docker: run the whole pi process in a container.
  • OpenShell: run the whole pi process in a policy-controlled sandbox.

So a fresh Pi install runs every tool call without asking.

The hook: tool_call and tool_result

Permissions in Pi are extensions. Extensions are TypeScript files Pi loads directly, with no build step. Pi’s extensions doc lists where they load from:

Path Scope Trust
~/.pi/agent/extensions/*.ts (or */index.ts) Global, every project Loads immediately
.pi/extensions/*.ts Project-local Loads only after the project is trusted
pi -e ./my-extension.ts One run For quick tests

Put a permission extension in the global path. It is then active before any repo opens, and declining project trust cannot skip it.

Two events carry the whole permission story:

Event Fires What a handler can return
tool_call Before the tool runs { block: true, reason?, terminate? } to deny. Mutate event.input in place to rewrite arguments.
tool_result After the tool runs Partial patches of content, details, structuredContent, isError or usage to rewrite or redact what the model reads

Handlers chain. Later tool_call handlers see earlier changes, and tool_result handlers work like middleware. Pi has no MCP layer of its own and no second tool path, so bash, read, write, edit and any extension-registered tool all pass through these events. One caveat from the doc: arguments are not re-validated after you change them, and unknown tools or invalid arguments skip tool_call entirely.

Two more hooks matter:

  • pi.setActiveTools(["read", "bash"]) limits which tools the model has at all.
  • before_agent_start can edit selectedTools and the system prompt before the agent runs, so a denied tool is never offered.

Is anyone there to answer: ctx.hasUI

An ask needs a person to answer it. Pi tells the extension whether one is there:

ctx.mode ctx.hasUI
tui (interactive) true
rpc true
print (pi -p) false
json false

With a UI, an extension prompts through ctx.ui.confirm(title, message) (returns a boolean) or ctx.ui.select(title, options). Without one, it has to decide alone. For a permission check, the safe answer is to block. Don’t hang, and don’t approve.

The permission gate example

Searches for “pi permission gate” land on permission-gate.ts, an example extension in Pi’s repo. It does this:

  1. Registers a tool_call handler and ignores everything except the bash tool.
  2. Tests the command against case-insensitive patterns for recursive rm, any sudo, and chmod or chown with 777.
  3. No match: returns nothing, and the command runs.
  4. Match with no UI: blocks, with a reason saying no UI was available to confirm.
  5. Match with a UI: shows a select prompt with the command and Yes / No. Anything other than Yes blocks with the reason “Blocked by user”.

A minimal gate in the same shape:

import { type ExtensionAPI, isToolCallEventType } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("tool_call", async (event, ctx) => {
    if (!isToolCallEventType("bash", event)) return;
    const cmd = event.input.command;
    if (!/\brm\s+-[a-z]*r|\bsudo\b/i.test(cmd)) return;
    if (!ctx.hasUI) return { block: true, reason: "Needs confirmation; no UI available" };
    const ok = await ctx.ui.confirm("Run this command?", cmd);
    if (!ok) return { block: true, reason: "Blocked by user" };
  });
}

Save it as ~/.pi/agent/extensions/gate.ts. The imports, pi.on("tool_call", ...), isToolCallEventType and the return shape come from the extensions doc. Returning nothing lets the call go ahead.

A gate is a pattern list, not a policy engine. Pi’s example covers three kinds of shell command and nothing else. For rules per tool, per command and per MCP server, use pi-permission-system.

pi-permission-system

pi-permission-system is a community extension, MIT-licensed, that gives Pi an allow/ask/deny policy file. Several forks exist with different config formats, notably @gotgenes/pi-permission-system. Everything below is the upstream MasuRii README. If you installed a fork, read its README instead.

Install

pi install npm:pi-permission-system

Or place it by hand at ~/.pi/agent/extensions/pi-permission-system (global) or .pi/extensions/pi-permission-system (project).

Config files

File Purpose
~/.pi/agent/pi-permissions.jsonc Global policy
<cwd>/.pi/agent/pi-permissions.jsonc Project policy
~/.pi/agent/agents/<agent-name>.md Global per-agent override, in permission: frontmatter
<cwd>/.pi/agent/agents/<agent-name>.md Project per-agent override
~/.pi/agent/extensions/pi-permission-system/config.json Extension settings: enabled, debug, yoloMode

JSONC comments and trailing commas are allowed.

Keys and values

The policy has six sections: defaultPolicy, tools, bash, mcp, skills, special. Each rule resolves to one of three values:

Value Effect
allow Runs silently
ask Prompts for confirmation in the UI
deny Blocks with an error message

defaultPolicy applies when no specific rule matches. A starting policy:

{
  "defaultPolicy": {
    "tools": "ask",
    "bash": "ask",
    "mcp": "ask",
    "skills": "ask",
    "special": "ask"
  },
  "tools": {
    "read": "allow",
    "write": "ask"
  },
  // broad first, specific last: the last matching rule wins
  "bash": {
    "*": "ask",
    "git status*": "allow",
    "git diff*": "allow",
    "npm test*": "allow",
    "rm -rf *": "deny",
    "sudo *": "deny"
  }
}

bash, mcp and skills are each an object mapping a * wildcard pattern to a value. Bash patterns match the full command string.

Matching and precedence

  • Last matching rule wins. Put broad fallbacks first and specific overrides after them.
  • tools.bash is the default for shell. bash patterns override it per command.
  • MCP rules match targets such as myServer or myServer:search, then fall back to tools.mcp and defaultPolicy.mcp. Tools an extension registers directly go under tools, not mcp.
  • Skills match by name with * wildcards. Denied skills are stripped from the prompt.
  • Layers apply in order: global policy, project policy, global agent frontmatter, project agent frontmatter. A project file cannot relax a deny from the global policy or global agent frontmatter.
  • Denied tools are hidden. On before_agent_start the extension removes denied tools from the active list and the system prompt. Unknown or unregistered tools are blocked before any permission check.
  • A broken config falls back to ask. If the file fails to parse, every category becomes ask, with a warning in the TUI.

The prompt and “Allow Always”

An ask offers Allow Once, Allow Always, Reject, and Reject with Reason. Allow Always lasts for the session only. It is held in memory, not written to disk. A configured deny is never relaxed by it.

Headless

ask needs a UI or a forwarded confirmation. For subagents without a UI, the extension forwards the request to the main interactive session, which shows the prompt and writes the answer back. With no interactive session at all, don’t count on asks being answered. Allow what your pipeline needs and deny the rest. yoloMode: true turns every ask into an approval, so use it only where an unattended run should approve everything that is not denied.

Limits

  • Pi itself enforces nothing. Every rule on this page lives in an extension. Remove the extension and Pi runs everything.
  • Pattern rules see the command string, not what it does. The pi-permission-system README says it cannot authorize behaviour hidden inside executables, sourced files, eval, package scripts or Git hooks. An npm test that runs a destructive script is still npm test.
  • Not a sandbox. The README and Pi’s own docs both say so. Pair permissions with Gondolin, Docker or OpenShell when a mistake would be costly.
  • Extensions run with your full permissions. Pi’s docs warn that extensions can execute arbitrary code. A project-local extension loads only after you trust the project, so read it first.
  • No record. None of these keep a ledger of what was allowed, asked or blocked, except pi-permission-system’s debug log and forks that add JSONL logging.

What a hook can add beyond rules

A tool_call handler can call a policy service, decide per call with the full arguments, and write every decision off the machine. That is what the ACP extension for Pi does. On an ask, it blocks the call in Pi. You approve on the console or from the approval email, then tell pi to retry. When ctx.hasUI is false, such as pi -p, an ask becomes a deny. The walkthrough is in the Pi ACP extension post.

Cost tracking works differently. pi-acp points Pi at a model provider entry in ~/.pi/agent/models.json that routes model calls through ACP’s broker. See Pi cost tracking. The install box below sets up both in one line.

Frequently asked questions

What is pi-permission-system?

A community extension for the Pi coding agent that adds allow, ask and deny rules for tools, bash commands, MCP, skills and special operations. Install it with pi install npm:pi-permission-system and write the policy in ~/.pi/agent/pi-permissions.jsonc, with an optional project file at .pi/agent/pi-permissions.jsonc.

pi permissions: does Pi have a built-in permission system?

No. Pi’s README says it does not include a built-in permission system for restricting filesystem, process, network or credential access, and that it runs with the permissions of the user who launched it. Permissions are added by extensions, or bounded by running Pi in a container or micro-VM.

What is the pi permission gate?

permission-gate.ts is an example extension in Pi’s repository. It listens on the tool_call event, checks bash commands against patterns for recursive rm, sudo, and chmod or chown 777, asks Yes or No when a UI is present, and blocks the command when there is no UI.

pi permission extension: how does an extension block a tool call?

It subscribes to tool_call, which fires before every tool runs, and returns { block: true, reason }. It can also mutate event.input to rewrite arguments, and a tool_result handler can patch what the model reads back.

pi harness permissions in headless mode

Extensions read ctx.hasUI, which is false in print (-p) and JSON modes. The permission-gate example blocks by default there. pi-permission-system says ask still needs a UI or a forwarded confirmation from an interactive session.

Where to read more

Running pi? One command adds a hook. Each tool call it sees is recorded, and a force-push, destructive SQL or curl | sh is blocked until you approve it on the console or from the email.

macOS · Linux · WSL
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
Windows · PowerShell
irm https://agenticcontrolplane.com/install.ps1 | iex

What the installer writes  ·  pi guide  ·  free up to 5 agents

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
Related posts

← back to blog