Skip to content

Pi Permissions

Configure Pi tool permissions via the pi-guardrails extension (forked from @aliou/pi-guardrails to fix RPC mode) and ~/.pi/agent/extensions/guardrails.json.

Permission Model

Pi allows all tool calls by default — no prompts, no gates. The guardrails extension adds gates for specific dangerous operations. Two tiers:

  1. Confirm — Operations where uncommitted or committed work could be permanently lost. Prompts for confirmation in interactive mode, blocks in headless/RPC mode. The test: "if this runs at the wrong time, can work disappear?" If yes, it belongs here.

  2. Deny — Catastrophic operations that should never run under any circumstances. Blocked silently, no prompt.

Everything else is auto-allowed. There is no explicit allow list because Pi doesn't prompt by default — there's nothing to suppress.

Comparison to Claude Code

Claude Code starts locked down (everything prompts unless allowed). Pi starts open (everything runs unless blocked). The guardrails extension bridges the gap by adding confirmation prompts for dangerous commands.

Claude CodePi equivalent
Allow list (auto-approve)Default behavior (no config)
Ask list (prompt)permissionGate.patterns (confirm)
Deny list (block)permissionGate.autoDenyPatterns (block)
File protectionpolicies.rules (noAccess/readOnly)

Install

sh
pi install git:github.com/chriscalo/pi-guardrails

This persists to ~/.pi/agent/settings.json and loads in every session automatically. The only bypass is pi --no-extensions.

Structure

Config lives at ~/.pi/agent/extensions/guardrails.json (global) or .pi/extensions/guardrails.json (project-local). The extension merges all scopes: global → project → memory (session). Later scopes win.

json
{
  "permissionGate": {
    "patterns": [
      { "pattern": "...", "description": "..." }
    ],
    "autoDenyPatterns": [
      { "pattern": "..." }
    ],
    "allowedPatterns": [
      { "pattern": "..." }
    ]
  },
  "policies": {
    "rules": [
      {
        "id": "...",
        "patterns": [{ "pattern": "..." }],
        "protection": "noAccess"
      }
    ]
  }
}

Patterns use substring matching by default. Add "regex": true for regex matching. Permission gate patterns are checked against bash commands. Policy patterns are checked against file paths.

Built-in defaults (always active)

The extension ships with these. No configuration needed — they're always on unless explicitly disabled.

Built-in: dangerous command detection

Matched structurally via AST parsing (not regex) for fewer false positives. Prompts for confirmation in interactive mode, blocks in headless/RPC mode.

rm -rf         — recursive force delete
sudo           — superuser command
dd if=         — disk write operation
mkfs.          — filesystem format
chmod -R 777   — insecure recursive permissions
chown -R       — recursive ownership change

Built-in: secret file protection

Blocks read, write, edit, bash, grep, find, and ls on matching paths. The agent sees a block message instead of the file contents.

.env             — blocked (noAccess)
.env.local       — blocked
.env.production  — blocked
.env.prod        — blocked
.dev.vars        — blocked
.env.example     — allowed (exception)
.env.sample      — allowed
.env.test        — allowed

Confirm (prompt for confirmation)

Custom patterns added to permissionGate.patterns. These extend the built-in list — they don't replace it.

Confirm: git operations that rewrite remote history

Force push rewrites the remote branch. If others have based work on commits that get replaced, their work can be silently lost.

git push --force   — force push (long flag)
git push -f        — force push (short flag)

Confirm: git operations that discard uncommitted changes

Edits that exist nowhere in git history are permanently lost.

git reset --hard   — discard staged and unstaged changes
git checkout --    — overwrite working tree files
git clean          — delete untracked files (never committed)

Confirm: git operations that delete branches, tags, worktrees

Refs and directories are permanently gone. Unmerged branch commits survive in the reflog temporarily, but tags and worktree directories do not.

git branch -d      — delete branch
git branch -D      — force-delete branch
git branch --delete — delete branch (long flag)
git tag -d         — delete tag
git tag --delete   — delete tag (long flag)
git worktree remove — delete a worktree directory

Confirm: git operations that destroy stash entries

Stash entries are saved work. drop and clear delete them permanently, and pop deletes the entry after applying.

git stash drop     — delete a single stash entry
git stash pop      — apply and delete a stash entry
git stash clear    — delete all stash entries

Confirm: GitHub CLI destructive and write API operations

Can delete repos and releases, or mutate remote state via raw API calls.

gh repo delete         — permanently delete a repository
gh release delete      — delete a release
gh api -X DELETE       — raw API DELETE request
gh api -X POST         — raw API POST request
gh api -X PUT          — raw API PUT request
gh api -X PATCH        — raw API PATCH request
gh api --method DELETE — raw API DELETE (long flag)
gh api --method POST   — raw API POST (long flag)
gh api --method PUT    — raw API PUT (long flag)
gh api --method PATCH  — raw API PATCH (long flag)

Confirm: file mutations that bypass git

In-place edits bypass git's safety net.

sed -i             — edit file in place (short flag)
sed --in-place     — edit file in place (long flag)

Deny (block entirely)

Custom patterns added to permissionGate.autoDenyPatterns. Blocked silently — no prompt, no option to override.

rm -rf /   — destroy entire filesystem

File policies

Custom rules added to policies.rules. Each rule has a unique id for deduplication across config scopes.

SSH keys and configuration

json
{
  "id": "ssh-keys",
  "description": "SSH private keys and configuration",
  "patterns": [
    { "pattern": ".ssh/" },
    { "pattern": "id_rsa" },
    { "pattern": "id_ed25519" },
    { "pattern": "id_ecdsa" }
  ],
  "allowedPatterns": [
    { "pattern": "*.pub" }
  ],
  "protection": "noAccess",
  "onlyIfExists": true,
  "blockMessage": "Accessing {file} is not allowed. This file contains SSH keys."
}

Policy protection levels:

  • noAccess — blocks read, write, edit, bash, grep, find, ls
  • readOnly — blocks write, edit, bash (allows reading)
  • none — explicit no protection (override a higher-scope rule)

When multiple rules match the same file, strongest protection wins: noAccess > readOnly > none.

What's NOT gated (deliberate)

Claude Code's ask list includes general-purpose scripting commands (python3, bash, sh, xargs, npx, npm exec, volta). These are not added to Pi's guardrails because the permission models are inverted:

  • Claude Code prompts on everything by default, so scripting commands need to be in the ask list to get a prompt instead of being silently blocked.
  • Pi allows everything by default. Adding python3 or bash to the dangerous patterns list would prompt on every script execution — too noisy for the open-by-default model.

If a script does something dangerous (e.g., python3 -c "import os; os.system('rm -rf /')") the guardrails extension catches the dangerous part via compound command analysis.

Confirmation UX

When a dangerous command is detected in interactive mode:

  • y/Enter — allow this once
  • a — allow for the rest of this session (saved in memory scope, forgotten when Pi exits)
  • n/Esc — deny (block the command)

In headless/RPC mode (no UI), dangerous commands are blocked by default — no way to confirm. This is the safe fallback for dashboard-spawned sessions.

Optional: enable LLM-powered command explanation in the confirmation dialog:

json
{
  "permissionGate": {
    "explainCommands": true,
    "explainModel": "anthropic/claude-haiku-4-5",
    "explainTimeout": 5000
  }
}

Full config file

The complete ~/.pi/agent/extensions/guardrails.json with all custom patterns from the sections above:

json
{
  "permissionGate": {
    "patterns": [
      {
        "pattern": "git push --force",
        "description": "force push (rewrites remote history)"
      },
      {
        "pattern": "git push -f",
        "description": "force push (rewrites remote history)"
      },
      {
        "pattern": "git reset --hard",
        "description": "discard all uncommitted changes"
      },
      {
        "pattern": "git checkout --",
        "description": "overwrite working tree files"
      },
      {
        "pattern": "git clean",
        "description": "delete untracked files"
      },
      {
        "pattern": "git stash drop",
        "description": "delete stash entry"
      },
      {
        "pattern": "git stash pop",
        "description": "apply and delete stash entry"
      },
      {
        "pattern": "git stash clear",
        "description": "delete all stash entries"
      },
      {
        "pattern": "git branch -d",
        "description": "delete branch"
      },
      {
        "pattern": "git branch -D",
        "description": "force-delete branch"
      },
      {
        "pattern": "git branch --delete",
        "description": "delete branch"
      },
      {
        "pattern": "git tag -d",
        "description": "delete tag"
      },
      {
        "pattern": "git tag --delete",
        "description": "delete tag"
      },
      {
        "pattern": "git worktree remove",
        "description": "delete worktree directory"
      },
      {
        "pattern": "gh repo delete",
        "description": "permanently delete a GitHub repository"
      },
      {
        "pattern": "gh release delete",
        "description": "delete a GitHub release"
      },
      {
        "pattern": "gh api -X DELETE",
        "description": "GitHub API DELETE request"
      },
      {
        "pattern": "gh api -X POST",
        "description": "GitHub API POST request"
      },
      {
        "pattern": "gh api -X PUT",
        "description": "GitHub API PUT request"
      },
      {
        "pattern": "gh api -X PATCH",
        "description": "GitHub API PATCH request"
      },
      {
        "pattern": "gh api --method DELETE",
        "description": "GitHub API DELETE request"
      },
      {
        "pattern": "gh api --method POST",
        "description": "GitHub API POST request"
      },
      {
        "pattern": "gh api --method PUT",
        "description": "GitHub API PUT request"
      },
      {
        "pattern": "gh api --method PATCH",
        "description": "GitHub API PATCH request"
      },
      {
        "pattern": "sed -i",
        "description": "in-place file edit (bypasses git)"
      },
      {
        "pattern": "sed --in-place",
        "description": "in-place file edit (bypasses git)"
      }
    ],
    "autoDenyPatterns": [
      { "pattern": "rm -rf /" }
    ]
  },
  "policies": {
    "rules": [
      {
        "id": "ssh-keys",
        "description": "SSH private keys and configuration",
        "patterns": [
          { "pattern": ".ssh/" },
          { "pattern": "id_rsa" },
          { "pattern": "id_ed25519" },
          { "pattern": "id_ecdsa" }
        ],
        "allowedPatterns": [
          { "pattern": "*.pub" }
        ],
        "protection": "noAccess",
        "onlyIfExists": true,
        "blockMessage": "Accessing {file} is not allowed. This file contains SSH keys."
      }
    ]
  }
}

Sync Permissions

To sync this config to a machine:

  1. Install the extension: pi install git:github.com/chriscalo/pi-guardrails
  2. Copy the JSON above to ~/.pi/agent/extensions/guardrails.json
  3. Verify by starting a Pi session and running a gated command (e.g., git push --force) — it should prompt for confirmation.

To check current config interactively: /guardrails:settings

Managing config

sh
# Interactive settings editor
/guardrails:settings

# Add a policy rule with AI assistance
/guardrails:add-policy

Source reference

  • Fork: github.com/chriscalo/pi-guardrails (forked from @aliou/pi-guardrails v0.9.5). Replaced ctx.ui.custom() with ctx.ui.select() so the confirmation dialog works in both TUI and RPC mode. Upstream bug: aliou/pi-guardrails#19.
  • Extension source: src/hooks/permission-gate.ts
  • Config schema: src/config.ts
  • File policies: src/hooks/policies.ts
  • Pi tool_call event: pi-coding-agent/dist/core/extensions/types.d.tsToolCallEvent, ToolCallEventResult ({ block, reason })