pi-unbash
A highly secure bash confirmation extension for pi, using the unbash AST parser to perfectly intercept subshells and logic gates.
Package details
Install pi-unbash from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-unbash- Package
pi-unbash- Version
2.0.0- Published
- Apr 30, 2026
- Downloads
- 73/mo Β· 18/wk
- Author
- jdiamond
- License
- MIT
- Types
- extension
- Size
- 43.6 KB
- Dependencies
- 1 dependency Β· 1 peer
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-unbash π‘οΈ
Deprecated. This package has been superseded by pi-guard. pi-guard has a superset of the same features plus broader threat detection (forbidden paths, environment variable inspection, etc.). No further updates will be published for pi-unbash. Please migrate to pi-guard.
A high-security, AST-powered bash confirmation extension for the pi coding agent.
Why pi-unbash?
Most bash confirmation extensions rely on simple string matching, regular expressions, or custom lexers to determine what commands an AI is trying to run. Those approaches can work for many cases, but they tend to get brittle once shell syntax becomes deeply nested or heavily composed. If an AI generates a command like:
FOO=bar echo "$(git status && `rm -rf /`)"
it is not enough to notice that the raw string contains suspicious text somewhere. The harder problem is to comprehensively extract the embedded commands that will actually execute, even when they are buried inside substitutions, pipelines, redirects, or control-flow syntax.
pi-unbash is different. It uses unbash, a fast, zero-dependency TypeScript parser that generates a full POSIX-compliant Abstract Syntax Tree (AST). pi-unbash recursively traverses that tree to extract embedded commands no matter how complicated the full shell command becomesβacross pipes (|), logic gates (&&, ||), subshells ($(), `...`), heredocs, and more.
That same AST also makes the approval prompt easier to read: instead of showing only the raw LLM-generated shell string, pi-unbash can format the extracted commands into a clearer, more compact preview that is easier to approve or reject in the terminal UI.
If the AI tries to sneak an unapproved command past you, pi-unbash will catch it and block execution until you explicitly confirm it via the terminal UI.
Installation
You can install pi-unbash globally into your pi settings:
# Install globally
pi install npm:pi-unbash
# Or install locally for testing
pi -e ./path/to/pi-unbash
Usage
By default, pi-unbash allows a set of safe, read-only commands to execute silently. See src/defaults.ts for the full list.
If the AI attempts to run anything else (e.g., git commit, npm, rm, node), the execution is paused, and a confirmation dialog appears in your pi session:
β οΈ Unapproved Commands
β cd /Users/β¦/project
β npm test
β git commit -A -m "update files"
β Allow
Always allow npm, git (this session)
Reject
Allow runs the command once. Always allow X (this session) adds the base command(s) to an in-memory allowlist for the rest of the session β no prompts for that command again until you reload. Reject blocks execution.
Configuration
Settings can be configured at two levels:
- Global:
~/.pi/agent/settings.jsonβ applies to all projects - Project:
<project>/.pi/settings.jsonβ applies only to that project
Project settings override global settings. Rules merge in order: defaults β global rules β project rules β session rules. Last match wins.
Global Settings
Settings are persisted globally in ~/.pi/agent/settings.json under the "unbash" key:
{
"packages": [
"npm:pi-unbash"
],
"unbash": {
"enabled": true,
"rules": {
"npm test": "allow"
}
}
}
Project Settings
Project-level settings go in .pi/settings.json in your project root:
{
"unbash": {
"rules": {
"npm run build": "allow",
"npm run test": "allow"
}
}
}
Project settings override global settings and can be committed to version control to share with your team.
Rules
The rules object maps command patterns to actions. Only your personal overrides go here β the built-in defaults (see src/defaults.ts) are always applied first and never written to disk, so you automatically benefit from future default updates.
Rules are evaluated in order: built-in defaults first, then your rules entries, last match wins. This means entries later in your rules object override earlier ones and override any matching default.
Actions:
"allow"β run silently without prompting"ask"β prompt for approval (the default for anything unmatched)
The special pattern "*" matches any command:
"*": "allow"β trust all commands globally (opt out of pi-unbash)
Matching uses subsequence logic β the tokens in your rule must appear in order in the actual command, but extra flags and arguments anywhere in the sequence are permitted:
| Rule | Matches | Doesn't Match |
|---|---|---|
"git" |
all git commands | β |
"git status" |
git status, git status --short |
git commit -m "msg" |
"git branch --show-current" |
git branch --show-current, git branch -v --show-current |
git branch -D main |
"jira issue view" |
jira issue view PROJ-123, jira issue view --verbose PROJ-123 |
jira issue create |
"terraform apply --dry-run" |
terraform apply --dry-run, terraform apply -v --dry-run |
terraform apply, terraform apply --force |
Because last-match-wins, you can override a broad default with a narrower rule:
{
"unbash": {
"rules": {
"npm test": "allow",
"git": "allow",
"git push": "ask"
}
}
}
Here git push always prompts even though git (which would match all git commands) appears before it β the more specific entry comes last and wins.
Display Settings
The confirmation prompt elides long command arguments to keep the display readable:
- The command name is always shown in full.
- If the full command fits within
commandDisplayMaxLength, it is shown unchanged. - Otherwise, the formatter shrinks later tokens only as much as needed to fit the total budget.
- Path arguments (starting with
/,~/,./, or../) get path-aware middle elision that preserves the tail. - Other long arguments are prefix-truncated with
β¦only when needed. commandDisplayArgMaxLengthacts as the minimum per-token elision target, not a hard cap when there is still room in the overall display budget.- If the total display still exceeds
commandDisplayMaxLength, the whole string is hard-truncated.
{
"unbash": {
"commandDisplayMaxLength": 120,
"commandDisplayArgMaxLength": 40
}
}
commandDisplayMaxLengthβ total character budget for the display string (default:120).commandDisplayArgMaxLengthβ minimum per-token elision target when shrinking long arguments/heredocs to fit the overall display budget (default:40).
Commands
You can manage settings dynamically mid-session using the /unbash command:
/unbash allow <command>- Permanently allow a command (e.g.,/unbash allow gitor/unbash allow git status)/unbash toggle- Turn the entire confirmation system on or off/unbash list- Show current status, default rules, user rules (global), project rules, and session rules
License
MIT