Dumb Pi extensions
  • TypeScript 99.9%
  • Shell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-02 10:10:26 +00:00
extensions bash-permissions: use effective provider for LLM judge calls 2026-08-22 23:35:25 +01:00
mise-tasks Add mise.toml and task to update Pi dependencies 2026-07-12 16:17:55 +01:00
test-utils bash-permissions: use effective provider for LLM judge calls 2026-08-22 23:35:25 +01:00
AGENTS.md Add mise.toml and task to update Pi dependencies 2026-07-12 16:17:55 +01:00
mise.toml Update Node.js to v26.5.1 2026-07-29 15:38:37 +00:00
package-lock.json Update dependency web-tree-sitter to ^0.27.0 2026-08-30 17:38:41 +00:00
package.json Update dependency web-tree-sitter to ^0.27.0 2026-08-30 17:38:41 +00:00
README.md add optional auto-mode to bash-permissions extension 2026-08-22 22:09:19 +01:00
renovate.json Group together Pi renovate updates 2026-07-19 16:19:31 +01:00
tsconfig.json perform typechecking before tests 2026-06-11 14:44:39 +01:00
vitest.config.ts Update vitest to v4.1.10 2026-07-12 16:17:49 +01:00

Pi extensions

My dumb Pi extensions.

Extension Description
bash-permissions Gates the bash tool behind allow/ask/deny rules with interactive approval and an opt-in LLM auto-approve judge
edit-diff Modifies the edit tool to provide the LLM a diff with context after a change has been made
edit-permissions Gates file edits behind user approval and protects the project's .pi directory
plan-mode Basic plan mode which disables edit tools (but not bash)
subagent (disabled) Allows the LLM to create subagents for performing various tasks (e.g. codebase exploration) with specific sets of tools and models
web-tools Allows the LLM to search the web and fetch content from the web. Supports HTTP pages via Exa or Kagi, and clones GitHub repositories

bash-permissions

Every bash tool call is parsed with tree-sitter-bash and split into its constituent simple commands (pipeline segments, &&/;/|| list members, subshells, command/process substitutions — including ones nested in strings, heredocs, and environment assignments). Commands embedded in find -exec/-execdir/-ok/-okdir clauses and rg --pre preprocessors are extracted and checked as separate commands too, so find * alone never approves an -exec payload and rg alone never approves a --pre payload. Each command is checked against the deny/ask/allow rules; anything denied is blocked, anything matched by an ask rule prompts for confirmation, and anything unmatched triggers an approval prompt:

  • Allow once — run it, persist nothing
  • Always allow (project) / Always allow (global) — persist an allow rule (only offered for unmatched commands; ask-matched commands can only be allowed once)
  • Deny (or dismissing the dialog) — block the tool call

A command that fails to parse is treated as one opaque unit and can only be allowed once or denied — no rule is ever persisted from unparseable input.

Output redirections that may create or modify a file (>, >>, >|, &>, &>>, and file-targeting >&) use the same policy and session decision as the edit-permissions extension: Allow once, Allow all edits for session, Deny once, or Deny all edits for session. Edit permission and command permission are independent, so allowing the write does not approve an unmatched or ask-matched command. As with edit/write tool calls, the write check permits redirects when no interactive UI is available unless the session already has a deny-all decision; ordinary bash no-UI rules still apply.

The edit check is not applied to harmless routes: input redirects, heredocs/herestrings, descriptor duplication or closure, process substitution, or known output sinks (/dev/null, standard-output and standard-error device/fd paths). A dynamic destination is treated conservatively as a possible file write.

Use /bash-permissions to list the loaded rules and their source files.

Rule files

Scope Path
project <cwd>/.pi/bash-permissions.json
global ~/.pi/agent/bash-permissions.json

Both files are loaded and merged on every check, so hand edits apply immediately. Format:

{
  "allow": ["git status", "less *", "npm run *", "sed *"],
  "ask": ["rm *"],
  "deny": [
    "sudo *",
    {"command": "perl *", "reason": "use read/edit/write tools instead of perl"}
  ],
  "commandPolicies": {
    "sed": {
      "normal": "allow",
      "inPlace": "ask"
    }
  }
}

deny entries may be either a plain string or an object { "command": ..., "reason": ... }. When a reason is present it is appended to the block message (e.g. bash-permissions: denied by rule: perl -pi file — use read/edit/write tools instead of perl) and shown next to the rule in /bash-permissions. allow and ask remain plain strings.

Command policies

commandPolicies applies semantic allow/ask/deny decisions to command modes which are awkward to express with token-prefix rules. sed is the only supported command initially:

  • normal — no in-place option was detected.
  • inPlace — -i, -I (BSD), --in-place, or a bundled/suffixed form such as -ni or -i.bak was detected. A dynamic token before -- is classified conservatively as inPlace because its runtime value could enable the mode.

The classifier recognizes sed by basename, so /usr/bin/sed is covered too. Each mode is optional; an unconfigured mode falls back to the ordinary command rules. Policies and rules combine conservatively using deny > ask > allow, and project/global policy conflicts use the same ordering. Thus an "inPlace": "ask" policy still prompts under allow: ["sed *"], while a deny rule still wins over normal: "allow". Policy-triggered prompts are one-shot and block when no UI is available.

normal means only that no in-place option was found; it is not a guarantee that an arbitrary sed program has no side effects. In particular, sed programs can write with w/W, and some implementations support command execution. Use ordinary ask/deny rules if untrusted sed programs must not run.

Matching semantics

Rules are whitespace-separated token lists matched against the parsed tokens of each command:

  • A * as the final token matches zero or more remaining arguments: less * matches less, less foo.txt, and less -N foo.txt, but never lessc x. Anywhere else, * is a literal.
  • Without a trailing * the match is exact: git status does not match git status -sb.
  • Deny decisions win over ask decisions, which win over allow decisions, including decisions from commandPolicies.
  • Tokens are matched as raw source text, quotes included.

Caveats

  • Wrapper commands other than find are matched as themselves: sudo rm -rf / matches rules for sudo, not rm. Don't allow-list sudo *, env *, xargs *, timeout *, etc. unless you mean it. (find and rg are special-cased: find's -exec-style payloads and rg's --pre preprocessor are checked as separate commands.)
  • Constructs that run nothing by themselves are implicitly allowed: standalone variable assignments, [[ … ]], and arithmetic. Any command substitution inside them is still extracted and checked.
  • The project rules file is honored only when the project is trusted (ctx.isProjectTrusted()). In untrusted projects it is ignored entirely and the prompt offers no "Always allow (project)" option.

LLM auto-approve

An opt-in LLM judge can auto-approve invocations whose commands match no rule. It is configured in the global rules file only — the whole llmApprove block is ignored in project files, so a project cannot enable, tune, or weaken the feature:

{
  "llmApprove": {
    "enable": true,
    // Optional. Omitted/null = the active session model; otherwise an
    // absolute "provider/model-id" string.
    "model": null,
    // Optional, default "high". One of minimal|low|medium|high|xhigh|max.
    "thinking": "high",
    // Optional, default 2000.
    "maxTokens": 2000,
    // Optional, default 30 (seconds).
    "timeout": 30,
    // Optional path to a prompt file that REPLACES the built-in judge
    // instructions. Relative paths resolve against the config file's directory.
    "prompt": "./judge-prompt.md"
  }
}

Behavior:

  • The judge is consulted only when every command in the invocation is allowed or unmatched and at least one is unmatched. Any deny segment still blocks and any ask segment still prompts — explicit rules always win over the judge. Unparseable commands are never judged, and output-redirection write checks run regardless of the verdict.
  • The judged unit is the full invocation (cwd + raw command string only — no conversation history or other context). Verdicts are strict JSON with approve, deny, or unsure.
  • approve runs the invocation without prompting; the tool result is prefixed with auto-approved by LLM judge: <reason> so it shows up in session history. deny blocks like a user deny (LLM judge denied: <reason>). unsure falls through to the normal approval prompt, with the reason shown in the dialog.
  • Outcomes are cached per session by exact command string (errors are not).
  • API failures, timeouts, malformed JSON, and unknown verdicts fall back to normal prompting with a warning (shown once per session); when no UI is available they block.

The built-in safety criteria approve only bounded side effects: read-only operations anywhere, plus writes confined to the workspace and reversible (e.g. local file edits, local git commits). Anything else — network state changes, package installs, privilege escalation, irreversible deletes — gets deny or unsure. A custom prompt file replaces these instructions entirely; keep it conservative.

Use /bash-permissions to see whether LLM auto-approve is enabled and which model it uses.

Testing judge prompts

A small harness judges a list of commands without starting a Pi session:

node scripts/judge-harness.ts <config.json> <cases.jsonl>

<config.json> is a rules file with an llmApprove block ("enable": true, plus the model/thinking/prompt settings under test). <cases.jsonl> holds one case per line, e.g. {"command": "rm -rf /", "expectedVerdict": "deny"}. Each command is judged with the working directory set to the current directory; the harness prints command, expected verdict, actual verdict, and the judge's reason, and exits non-zero on any mismatch.

Flags

--bash-permissions <block|allow> controls what happens to unmatched commands when no interactive UI is available (e.g. print mode). The default is block. Ask rules always block when there is no UI to confirm them. This flag controls command authorization only; redirection writes follow the edit-permissions session policy described above.

edit-permissions

The write and edit tools require interactive approval unless the session already has an allow-all or deny-all edit decision. When no interactive UI is available, writes are allowed unless the session has a deny-all decision.

Calls targeting <cwd>/.pi or anything beneath it are always blocked before the normal permission policy runs. This protection applies to relative, absolute, and normalized paths and cannot be bypassed by an allow-all session decision or by running without a UI. It applies specifically to write and edit tool calls; bash output redirections continue to use the shared session permission policy described under bash-permissions.

subagent

Each agent's model: frontmatter field names a model type rather than a concrete model. At spawn time, the type is resolved to an actual model string and passed to the spawned pi as --model <value>.

Resolution order

  1. Environment variable AGENT_MODEL_<TYPE> (case-sensitive, must match the type name exactly).
  2. Entry in ~/.pi/agent/subagent.json under the type name.
  3. For the type default only: whatever model Pi is currently using (ctx.model.id). This lets an agent inherit the main session's model.
  4. No match → no --model flag is set; the spawned pi uses its own default.

Example

Given an agent with model: small in its frontmatter, this config:

{
  "small": "deepseek/deepseek-v4-flash",
  "large": "openrouter/minimax/minimax-m3"
}

…or this environment variable:

export AGENT_MODEL_small=deepseek/deepseek-v4-flash

…will cause the subagent to be spawned with --model deepseek/deepseek-v4-flash.

Env vars always take precedence over ~/.pi/agent/subagent.json. Missing or malformed config files and empty/non-string values are silently ignored.

Config reference (~/.pi/agent/subagent.json)

The file is a flat JSON object. Keys are model type names, values are model strings (e.g. "openai/gpt-4o-mini", "anthropic/claude-opus-4-5").

web-tools

Supports two search providers: Exa and Kagi. Select via searchProvider in ~/.pi/agent/web.json (defaults to "exa").

Exa setup

Set EXA_API_KEY env var or exaApiKey in ~/.pi/agent/web.json:

{
  "searchProvider": "exa",
  "exaApiKey": "abcd123"
}

Kagi setup

Set KAGI_API_KEY env var or kagiApiKey in ~/.pi/agent/web.json:

{
  "searchProvider": "kagi",
  "kagiApiKey": "abcd123"
}

Config reference (~/.pi/agent/web.json)

Field Type Default Description
searchProvider "exa" | "kagi" "exa" Which search provider to use
exaApiKey string — Exa API key (env: EXA_API_KEY)
kagiApiKey string — Kagi API key (env: KAGI_API_KEY)

Env vars always take precedence over config file values.