Dumb Pi extensions
  • TypeScript 99.8%
  • Shell 0.2%
Find a file
2026-07-20 10:11:04 +00:00
extensions bash-permissions: file redirects use edit-permissions policy 2026-07-11 16:42:29 +01:00
mise-tasks Add mise.toml and task to update Pi dependencies 2026-07-12 16:17:55 +01:00
test-utils tests: use real Pi types for mocks 2026-06-11 14:44:43 +01:00
AGENTS.md Add mise.toml and task to update Pi dependencies 2026-07-12 16:17:55 +01:00
mise.toml Add mise.toml and task to update Pi dependencies 2026-07-12 16:17:55 +01:00
package-lock.json Update dependency @types/node to v25.9.5 2026-07-19 15:21:40 +00:00
package.json Update dependency @types/node to v25.9.5 2026-07-19 15:21:40 +00:00
README.md bash-permissions: file redirects use edit-permissions policy 2026-07-11 16:42:29 +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
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
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.

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.

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.