opencode-shell-guard
A pre-execution guard that blocks destructive shell commands before they run, with the reason and a safer alternative.
The agent keeps working normally. Only commands that would destroy work, lose history, or run unreviewed remote code get stopped. Everything else passes through untouched.
Installation
OpenCode v2 loads the published package from the plugins array in opencode.json:
{
"plugins": ["@beloved-lenora/opencode-shell-guard@latest"],
}
Then restart the service (opencode service restart) so the plugin loads.
How it works
Every shell command is checked before execution:
- The command is parsed quote-aware, including chained segments (
&&, ||, ;, |) and command substitutions ($(...), backticks).
- Common wrappers (
sudo, env, bash -c, sh -c) are unwrapped so the real command is what gets judged.
- If a destructive pattern matches, the command is replaced with a short explanation naming the rule, why it fired, and what to do instead.
- Quoted text is treated as data.
echo 'rm -rf /' and grepped examples never trigger a block.
Unparseable commands are allowed through. The guard fails open rather than breaking your workflow.
What it blocks
Git history and worktree
reset --hard, --merge | Destroys uncommitted changes permanently | Stash first, then reset |
checkout -- <path>, checkout -f | Overwrites working-tree files permanently | Stash or diff first |
clean -f | Removes untracked files permanently | Preview with clean -n |
push -f, --delete | Rewrites or deletes remote history | Use --force-with-lease |
branch -D | Deletes a branch without a merge check | Use lowercase -d |
tag -d | Deletes a tag permanently | Delete tags by hand |
stash drop, stash clear | Permanently deletes stashed changes | Apply the stash first |
restore without --staged | Discards working-tree changes | Preview with diff, or use --staged |
switch -f, --discard-changes | Discards uncommitted changes | Stash or commit first |
Abbreviated flags match because git itself accepts unique prefixes. Dry runs (clean -n, --dry-run) and safe variants (--force-with-lease, lowercase -d) always pass.
Filesystem deletion
rm -rf /, ~, ., *, bare /tmp | Destroys the machine, home, or working tree | Delete a named subdirectory, list first |
rm -rf with $VAR or backtick targets | Target cannot be verified before running | Use literal paths |
rm -rf escaping with .. or absolute paths | Deletes outside the working tree | Stay inside the current directory |
--no-preserve-root | Explicitly disables the root-deletion guard | Never pass it |
find -delete, find -exec rm -rf | Deletes whatever the search happens to match | Preview matches first |
... | xargs rm -rf | Deletes whatever the pipe feeds it | Use an explicit file list |
Everyday cleanup stays allowed: rm -rf node_modules, rm -rf /tmp/my-dir, and rm -rf $TMPDIR/my-dir all pass.
Disks and devices
dd of=/dev/... | Overwrites disks or partitions |
mkfs ... | Formats a disk, destroying all data on it |
shred ... | Destroys file contents beyond recovery |
:(){ :|:& };: | Fork bomb hangs the machine |
Formatting and device writes are never agent tasks. Do them by hand.
Remote code and piped shells
curl ... | sh, wget ... | bash | Runs unreviewed remote code | Download first, read it, then run it |
... | xargs sh -c | Executes piped input as code | Avoid piping dynamic input into a shell |
Script one-liners
Code passed to interpreters (python -c, node -e, ruby -e, perl -e) gets the same checks as the equivalent shell command. Hiding rm -rf or git reset --hard inside a one-liner does not bypass the guard.
Examples
Blocked, with explanation:
$ rm -rf /
BLOCKED by shell-guard rm.recursive-catastrophic
Recursive rm targeting /, ~, or the working tree itself is always blocked.
Safer: Delete a named subdirectory instead, and preview with ls first.
Allowed, runs normally:
$ rm -rf node_modules
$ git push --force-with-lease
$ git clean -n
$ echo 'rm -rf /'
Philosophy
- Default allow. Only known-destructive patterns block. Unknown commands run.
- Fail open. If a command cannot be parsed, it runs.
- Explain, don't just deny. Every block names the rule, the reason, and the safer command.
- Stricter than git. Bare globs,
--delete pushes, and forced checkouts block even though some setups tolerate them. Typing the safe variant takes seconds; recovering history takes longer.