@lystran/pi-guard0.6.0
Protect Pi shell tool calls with configurable command guard rules
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
@lystran/pi-guard
Checks commands with destructive_command_guard's dcg --robot test interface before Pi executes the bash tool, with configurable confirmation or denial rules for specific commands
Installation
Install dcg according to its official documentation and verify that dcg --version works, then run this from the plugin directory:
pi install -l .
You can also add the published package to Pi's extension configuration
Integrated Plugins
@howaboua/pi-codex-conversion
Configuration
The plugin asks for confirmation when dcg considers a command dangerous by default. If no configuration exists, the plugin creates ~/.pi/agent/guard.json with this policy. Existing configuration files are never overwritten. Rule files are searched in this order:
- Project configuration:
.pi/guard.json - User configuration:
~/.pi/agent/guard.json PI_GUARD_CONFIG: Explicit configuration path
Example configuration:
{
"defaultMode": "confirm",
"headless": "deny",
"notify": { "includeCommand": false },
"rules": [
{ "command": "rm -rf *", "mode": "deny" },
{ "command": "git clean -fd *", "mode": "deny" }
]
}
defaultMode controls dangerous commands that do not match a specific rule. It defaults to confirm, so a confirmation dialog is shown for every command that dcg classifies as dangerous. A matching rule overrides defaultMode, so use mode: "deny" for commands that must never be approved interactively
Rules only apply after dcg has classified a command as dangerous, except that a matching mode: "confirm" rule also asks about commands dcg considers safe. dcg continues to evaluate and execute ordinary safe commands. Rules are matched in file order, and the first match wins. In a command containing *, * matches any number of characters
You can also explicitly use match: "exact", "prefix", "wildcard", or "regex". When match is omitted, commands without * use exact matching and commands containing * use wildcard matching
Optional environment variables: DCG_BIN, DCG_PI_MODE, DCG_PI_HEADLESS, DCG_PI_TIMEOUT_MS, DCG_PI_NOTIFY (on or off)
Commands are denied when dcg is missing, times out, returns malformed output, or exits with an unrecognized code. Pi displays a notification for direct denials, configuration errors, and canceled confirmations; confirmation dialogs show the command, dcg reason, and matching configuration rule
Notifications
The plugin delivers a notification right before a confirmation dialog appears, so a session that keeps running in a background terminal can still reach you. Notifications are sent in interactive TUI sessions on an interactive terminal only; print, JSON, and RPC modes are skipped, and a redirected stdout receives no terminal escape sequences. A notification is a best-effort side effect and never changes a guard decision
Delivery stops at the first layer that applies:
| Order | Layer | Applies when |
|---|---|---|
| 1 | Terminal bell | Inside tmux or GNU screen and bell is enabled |
| 2 | Terminal notification (OSC 99 on kitty, OSC 777 on the others) |
The terminal is known to render it |
| 3 | System notification chain | Neither layer above applies |
| 4 | Terminal bell | Every candidate in layer 3 failed and bell is enabled |
Inside tmux or GNU screen the bell is the only channel that works: both multiplexers drop the OSC notifications, but a bell becomes a window alert in the status line and, unless visual-bell is on, is passed through to the outer terminal, which can raise a native notification you can click to get back to the session. Layers 1 and 2 skip the system notification chain, so one confirmation never raises two notifications. Outside a multiplexer the price of the OSC layer is that a terminal escape sequence reports no failure, so the terminal list is a positive allowlist and anything unrecognised falls through to the system chain:
- Renders
OSC 99orOSC 777: kitty, Ghostty, WezTerm, iTerm2, Warp, rxvt-unicode - Excluded: tmux and GNU screen drop these sequences instead of passing them through, Windows Terminal ships
OSC 777disabled, and VS Code's xterm.js implements none of them; Apple Terminal and Alacritty are excluded for the same reason, andTERM=dumbdeclares no terminal capability at all - tmux is excluded by
TMUXand screen bySTY, not byTERM_PROGRAM: tmux rewritesTERM_PROGRAMbut leaves other terminal variables such asKITTY_WINDOW_IDin place, so only the multiplexer variables themselves are reliable
{
"notify": {
"enabled": true,
"includeCommand": false,
"minIntervalMs": 1500,
"maxPerMinute": 5,
"bell": true
}
}
enabledsends notifications,DCG_PI_NOTIFY=offdisables them for one runincludeCommandappends the truncated command text to the notification body, which keeps that command in the notification center, so it is off by default. Notification text is stripped of control characters and semicolons before it reaches a terminal escape sequence, so command text cannot end that sequence early or inject another oneminIntervalMsis the minimum delay between two notifications, andmaxPerMinutecaps a burst of confirmations; both apply to every layerbellwrites a terminal bell on an interactive terminal: that is the delivery mechanism inside tmux or GNU screen, and the last resort when no system notification was delivered anywhere else
Notification commands are spawned as separate processes with an argument vector, never through a shell, and each failure falls back to the next candidate in the chain:
| Platform | Candidates in order |
|---|---|
| macOS | terminal-notifier, osascript, afplay |
| Linux | notify-send, gdbus |
| Windows | terminal bell only |
terminal-notifier is not installed by default, so macOS without it falls back to osascript, whose notifications are attributed to the Script Editor notification permission. On Linux, notify-send needs libnotify and a running notification server. Every candidate is killed after five seconds, and a platform without a working candidate ends with the terminal bell
Boundaries
This is pre-execution protection for Pi bash tool calls, not an operating-system sandbox. It does not cover other custom tools, shells started directly by the user, or scripts that bypass tool calls; use Pi inside a container or OS sandbox when stronger isolation is required
Pi Guard registers nothing inside a subagent session hosted by a pi-subagents background runner, which sets PI_SUBAGENT_CHILD=1 before loading extensions. Such a session has no confirmation UI, so every command dcg classifies as dangerous would be denied outright instead of confirmed, which breaks unattended subagent work. Foreground subagents run inside the parent process and never load ambient extensions, so they are unaffected too; the only way Pi Guard still applies inside a subagent is an agent that lists it explicitly in extensions or subagentOnlyExtensions while running as a foreground child