@georgedong32/permission-modes

Claude-Code-style permission modes (ask / plan / auto) for the pi coding agent — with per-mode model profiles, outside-cwd write tracking, and per-mode skill filtering.

Packages

Package details

extension

Install @georgedong32/permission-modes from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@georgedong32/permission-modes
Package
@georgedong32/permission-modes
Version
2.4.0
Published
Jul 25, 2026
Downloads
249/mo · 189/wk
Author
georgedong32
License
MIT
Types
extension
Size
348.5 KB
Dependencies
0 dependencies · 4 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@georgedong32/permission-modes

npm version License

Claude-Code-style permission modes for the pi coding agent. Four modes, cycled with Shift+Tab, that control how tool calls get approved. v2.0.0 adds bypass, a built-in auto classifier, plan.md file driver, and system-prompt anchor injection.

Modes

Mode edit / write reads outside cwd bash notes
ask prompt (Allow / Allow all → bypass / Block) prompt mutating commands prompt one-line reminder on mode switch
plan only plan.md allowed read-only allowlist plan file at .pi/projects/<id>/plan.md
auto cwd writes auto; risky ops → classifier/blacklist auto tiered optional classifier in permission-modes.json
bypass all auto-approved (tracked outside cwd) auto auto security reminder on switch + compact

Cycle (Shift+Tab): ask → plan → auto → bypass → ask.

When there is no interactive UI (pi -p, --mode json), anything that would prompt is blocked instead of silently allowed.

Auto classifier (optional)

Configure ~/.pi/agent/permission-modes.json:

{
  "classifier": {
    "enabled": false,
    "model": "anthropic/claude-haiku-4-5",
    "timeoutMs": 8000
  }
}

Uses completeSimple from @earendil-works/pi-ai/compat with credentials from pi's modelRegistry — all built-in provider APIs (Anthropic, OpenAI, Azure, etc.) are supported automatically.

Permission rules (v2.1.0, CC-compatible)

Configure allow / deny / ask rules in Claude Code syntax (Tool or Tool(specifier)). Rules merge across scopes; evaluation order is deny → ask → allow (deny cannot be overridden).

Scope Path
Global ~/.pi/agent/permission-modes.jsonpermissions block
Project (shareable) <cwd>/.pi/projects/<id>/permissions.json
Project local <cwd>/.pi/projects/<id>/permissions.local.json (personal; add to .gitignore)

Example global config:

{
  "classifier": { "enabled": false },
  "permissions": {
    "allow": ["Bash(npm run test *)", "Read(~/.zshrc)"],
    "deny": ["Bash(curl *)", "Read(./.env)", "Read(./.git/**)"],
    "ask": ["Bash(npm install *)"]
  }
}

When a tool call would prompt, you can choose Allow always (this project) or Allow always (global) to persist an allow rule. Use /permissions to list the merged rule set.

Model profiles (v1.1.1)

Define named profiles in ~/.pi/agent/model-profiles.json mapping each mode to a model ID:

{
  "active": "default",
  "default": {
    "ask":  "anthropic/claude-opus-4-5",
    "plan": "anthropic/claude-opus-4-5",
    "auto": "anthropic/claude-haiku-4-5"
  },
  "fast": {
    "ask":  "anthropic/claude-haiku-4-5",
    "plan": "anthropic/claude-haiku-4-5",
    "auto": "anthropic/claude-haiku-4-5"
  }
}

A model ID is "provider/model" or "provider/model:thinking" (the :thinking suffix sets the thinking level after the switch — e.g. anthropic/claude-sonnet-4-5:high). When the mode changes, the extension auto-switches the model via pi.setModel(). The footer shows profile:<name> · model/thinking when a profile is active.

If the file doesn't exist on first install, the extension creates it for you (pre-filled with the user's default model from ~/.pi/agent/settings.json when available).

Why a separate model-profiles.json and not pi's built-in models.json? Pi uses ~/.pi/agent/models.json for custom provider definitions; using a different filename avoids format conflict.

Commands, shortcut, flag

Kind Name Behavior
Command /ask, /plan, /auto switch to that mode (/default also works as alias)
Command /mode [name] set the given mode, or pick from a list
Command /auto-depth <n> cap auto-mode follow-ups (0 = unlimited; default 20)
Command /model-profile show selector of available profiles
Command /model-profile <name> activate the named profile (also /model-profile list to print them)
Shortcut Shift+Tab cycle modes
Shortcut Alt+T cycle thinking level (off → minimal → low → medium → high → xhigh)
Shortcut Alt+I cycle model profile (next profile from ~/.pi/agent/model-profiles.json; re-applies the model for the current mode)
Command /permissions list merged allow/deny/ask rules
Command /outside-writes list tracked outside-cwd writes (read-only)
Command /undo-outside-writes restore outside-cwd writes (selector, all, or --list)
Flag --permission-mode <name> start in a mode (accepts ask, plan, auto, or default as alias; default ask)
Flag --model-profile <name> start with a named profile activated

The start-mode flag is --permission-mode (not --mode) because pi already has a built-in --mode for output format (text/json/rpc).

Outside-cwd write tracking (v1.1.3)

In auto mode, edit and write calls to paths outside the working directory are auto-approved — but each one is snapshotted to <cwd>/.pi/projects/<project-id>/tmp/outside-writes/. Use /undo-outside-writes to roll back:

  • /undo-outside-writes — interactive selector (newest first)
  • /undo-outside-writes all — restore all without prompting
  • /undo-outside-writes --list — list only (alias for /outside-writes)
  • /outside-writes — same as --list

Snapshots capture the file's pre-write content (or null if the file didn't exist). They persist across sessions until you undo them. The snapshot cap is 100 entries (oldest are evicted; you get a notification).

Per-mode skill filtering (v1.1.4, fixed in v1.1.5)

When you're in plan mode, skills like systematic-debugging and executing-plans are irrelevant noise. Skill filtering lets you configure which skills get injected into the system prompt per mode — saving tokens and keeping the agent focused.

{
  "active": "default",
  "default": {
    "ask": "anthropic/claude-sonnet-4-5",
    "plan": {
      "model": "anthropic/claude-sonnet-4-5",
      "skills": ["brainstorming", "writing-plans"]
    },
    "auto": "openai/gpt-4o"
  }
}
  • "skills": ["brainstorming", "writing-plans"] — only these two skills appear in the system prompt when in plan mode.
  • "skills": ["*"] or omitting skills entirely — allow all skills (default, no filtering).
  • String shorthand ("anthropic/claude-sonnet-4-5") still works — backward compatible.
  • Skills can still be invoked manually via /skill:name if needed — filtering only controls which skills are pre-loaded into the agent's context.

Resolution order: Active profile → default profile → hardcoded defaults (["*"]). The read tool is always mandatory in tool filters (full tool filtering is planned for a future release).

Plan mode flow

In plan mode the agent explores read-only and emits a numbered list under a Plan: header. On completion you choose:

  • Execute the plan — switches to auto, restores edit/write, runs the steps; a ☐/☑ widget advances as the agent emits [DONE:n] tags, and you get Plan Complete! ✓ at the end.
  • Stay in plan mode — keep iterating.
  • Refine the plan — opens an editor; your notes are sent back as a follow-up.

UI

  • A custom adaptive footer (v1.1.6): wide terminals show cwd [git-branch] · context usage · profile/model on line 1 and mode on line 2; narrow terminals reflow to two compact lines with truncation.
  • Context usage in the footer shows tokens/contextWindow percent (e.g. 42k/200k 21.0%; large counts use M).
  • While the agent is streaming, the working indicator shows live token / tok-s / cost / % context stats; it reverts to the default loader when idle.
  • Mode colors: ask = muted, plan = accent, auto = warning. The colored status pill was removed in v1.1.6 — mode is shown in the footer only.

Current mode, the auto-follow-up depth, and the active profile name persist across /reload and session resume.

Install / run

# Install as a package (scoped npm name; or from a git remote / local path)
pi install npm:@georgedong32/permission-modes
pi list                                            # verify it loaded

# Or run it directly for a quick try (no install)
pi -e ./extensions/permission-modes/index.ts

# During development, hot-reload after edits
/reload

Auto-discovery also works: drop this folder at ~/.pi/agent/extensions/permission-modes/ (global) or .pi/extensions/permission-modes/ (project) and pi loads index.ts automatically.

Testing

npm test                # run all 189 tests (vitest)
npm run test:watch      # watch mode for development

Changelog

See CHANGELOG.md for the full release history.

Layout

permission-modes/             # @georgedong32/permission-modes
├── package.json              # pi manifest + npm package metadata
├── index.ts                  # main extension (default-exported factory)
├── profiles.ts               # NEW in v1.1.1: model-profile config helpers
├── profiles.test.ts          # NEW in v1.1.1: unit tests for profiles
├── utils.ts                  # bash allowlist + Plan: + [DONE:n] helpers
├── index.test.ts             # integration tests (vitest)
├── utils.test.ts             # unit tests (vitest)
├── vitest.config.ts          # vitest config
├── CHANGELOG.md              # release history
├── LICENSE                   # MIT
├── .gitignore                # excludes node_modules, lockfile, .pi/
└── docs/
    ├── PRD.md                # product requirements
    └── prompts/              # mode-specific prompt context
        ├── ask-mode-prompts.md
        ├── plan-mode-prompts.md
        └── auto-mode-prompts.md

Third-party deps: none. Peer dependencies (bundled by pi): @earendil-works/pi-coding-agent, @earendil-works/pi-ai, @earendil-works/pi-tui, typebox. The model is switched only when the user opts in via ~/.pi/agent/model-profiles.json — without a profile config file, the model never changes and the extension behaves exactly as in v1.1.0.