pi-permission-system and Pi permissions: the reference
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
piand provider auth on the host and routes built-in tools and!commands into a local Linux micro-VM. - Docker: run the whole
piprocess in a container. - OpenShell: run the whole
piprocess 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_startcan editselectedToolsand 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:
- Registers a
tool_callhandler and ignores everything except thebashtool. - Tests the command against case-insensitive patterns for recursive
rm, anysudo, andchmodorchownwith777. - No match: returns nothing, and the command runs.
- Match with no UI: blocks, with a reason saying no UI was available to confirm.
- 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.bashis the default for shell.bashpatterns override it per command.- MCP rules match targets such as
myServerormyServer:search, then fall back totools.mcpanddefaultPolicy.mcp. Tools an extension registers directly go undertools, notmcp. - 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
denyfrom the global policy or global agent frontmatter. - Denied tools are hidden. On
before_agent_startthe 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-systemREADME says it cannot authorize behaviour hidden inside executables, sourced files,eval, package scripts or Git hooks. Annpm testthat runs a destructive script is stillnpm 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
- Pi README: the no-permission-system statement and containment options
- Pi extensions doc: events,
ctx.hasUI,ctx.ui, tool hiding, load paths - permission-gate.ts: the reference permission gate
- pi-permission-system: the upstream allow/ask/deny extension
- pi controls page: the full control model
- Install ACP for Pi: the extension and its paths
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.
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
irm https://agenticcontrolplane.com/install.ps1 | iex
What the installer writes · pi guide · free up to 5 agents