A standalone Oh My Pi extension that intercepts every bash tool call and blocks or challenges destructive commands before they run.
The plugin registers a tool_call handler. On every bash call it splits the command into segments on ;, &&, ||, |, and newlines (splitBashSegments in bash-policy.ts), then checks each segment through three tiers, in order:
- Hard blocks (
BASH_HARD_BLOCKS) — always denied, no override:git commit,git push,git reset --hard,git push --forcecargo publish,npm publish,pnpm publish,bun publish,yarn publishrm -rf /...sudo
- Project blocks (
BASH_PROJECT_BLOCKS) — denied only when the current working directory matches a project kind (detected vialib/project-kind.ts, which looks forCargo.toml,package.json,prisma/schema.prisma):cargo testin a Rust project → usecargo nextest runinstead
- Ask-first (
BASH_ASK) — denied with a reason telling the agent to surface the command to the user instead of running it itself:rm,git reset,curl,wget,chmod,dd,eval,sourcenpm install,pnpm install,bun install,pip install,cargo install
Any segment that matches returns { block: true, reason } from the tool_call handler, which the harness surfaces back to the agent instead of executing the command. Commands that match nothing pass through untouched.
Blocking the literal bash command isn't enough — bash ship.sh or python3 deploy.py can hide the exact same destructive commands one level down. After the three tiers above pass, each segment is checked for a script invocation (extractScriptTarget in bash-policy.ts):
- Interpreter form:
bash <file>,sh <file>,zsh <file>,source <file>,. <file>,python <file>/python3 <file>,bun <file>,node <file> - Direct execution:
./fileor an absolute path as the first token of the segment
If a segment matches, the file is resolved against the session's cwd (captured on session_start) and read from disk, capped at 1MB (node:fs openSync/readSync, no third-party deps). If the file can't be read, the call is blocked as ask-first — unreadable content is treated as unknown, not safe:
BLOCKED: '<segment>' invokes '<file>' ..., which could not be read. Its content is unknown — ask the user before running it.
Otherwise the content is scanned line by line (scanScriptContent):
- Shell scripts (
bash/sh/zsh/source/./bun/node/direct-exec targets that aren't.py) — every line is split with the samesplitBashSegmentsand checked againstBASH_HARD_BLOCKSthenBASH_ASK, exactly like a top-level bash command. - Python scripts (
python/python3/direct-exec targets ending in.py) additionally get:shutil.rmtree(...),os.remove(...),os.unlink(...)calls — flagged as ask-first file/directory deletion.os.system(...)andsubprocess.call/run/Popen/check_call/check_output(...)calls whose string argument matchesBASH_HARD_BLOCKSorBASH_ASK(e.g.os.system("git push"),subprocess.run("rm -rf /")) — flagged with the same reason as if that command had been run directly.
A match blocks with a reason naming both the script file and the offending line, e.g.:
BLOCKED: 'rm -rf /...' is catastrophic. Ask the user. Found in script '/tmp/evil.sh' (line 3), invoked via 'bash /tmp/evil.sh'.
Scripts with no match (e.g. a plain echo-only shell script) pass through untouched, same as any other command.
omp plugin install github:gyoz-ai/omp-bash-guardThis runs bun install against ~/.omp/plugins, symlinks the package into node_modules, and validates that index.ts resolves and exports a plugin factory before enabling it.
To work on this repo directly instead of through the plugin manager, symlink it into your OMP extensions directory — the repo root doubles as the extension directory (it contains index.ts at the top level, which is exactly what the loader expects for a single-plugin extension):
ln -s "$(pwd)/omp-bash-guard" ~/.omp/agent/extensions/bash-guard(Use ~/.omp/agent/extensions/ for a user-wide install, or <project>/.omp/extensions/ to scope it to one project.)
The extension loads automatically on the next session, and OMP live-reloads it whenever index.ts or bash-policy.ts changes.
index.ts— the plugin: registers thetool_callhook, trackscwd/project kind acrosssession_start, applies the three-tier bash policy, and resolves/reads script files for scanning (readScriptCapped,node:fsonly).bash-policy.ts— the policy data,splitBashSegments, and the script scanning logic (extractScriptTarget,scanScriptContent,PYTHON_ASK_PATTERNS): no side effects (no disk reads), easy to unit test or extend.lib/project-kind.ts— vendored copy of the project-kind detector used for the Rustcargo test→cargo nextest runproject block.
MIT — see LICENSE.