# pi-permission-system and Pi permissions: the reference

Pi ships no permission system. Extensions add it: the permission-gate example, pi-permission-system's allow/ask/deny file, and tool_call hooks.

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](/controls/pi).

## What Pi ships: no permission system, on purpose

Pi's [README](https://github.com/earendil-works/pi) 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](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md) 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](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts) 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:

```ts
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`](https://github.com/MasuRii/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`](https://www.npmjs.com/package/@gotgenes/pi-permission-system). Everything below is the upstream MasuRii README. If you installed a fork, read its README instead.

### Install

```sh
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:

```jsonc
{
  "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](/integrations/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](/blog/pi-acp-extension).

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](/blog/pi-cost-tracking). The install box below sets up both in one line.

## Frequently asked questions



## Where to read more

- [Pi README](https://github.com/earendil-works/pi): the no-permission-system statement and containment options
- [Pi extensions doc](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md): events, `ctx.hasUI`, `ctx.ui`, tool hiding, load paths
- [permission-gate.ts](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts): the reference permission gate
- [pi-permission-system](https://github.com/MasuRii/pi-permission-system): the upstream allow/ask/deny extension
- [pi controls page](/controls/pi): the full control model
- [Install ACP for Pi](/integrations/pi): the extension and its paths
