A Claude Code plugin that acts as a safety net, catching destructive git and filesystem commands before they execute.
We learned the hard way that instructions aren't enough to keep AI agents in check.
After Claude Code silently wiped out hours of progress with a single rm -rf ~/ or git checkout --, it became evident that "soft" rules in an CLAUDE.md or AGENTS.md file cannot replace hard technical constraints.
The current approach is to use a dedicated hook to programmatically prevent agents from running destructive commands.
Claude Code's .claude/settings.json supports deny rules for Bash commands, but these use simple prefix matching—not pattern matching or semantic analysis. This makes them insufficient for nuanced safety rules:
| Limitation | Example |
|---|---|
| Can't distinguish safe vs. dangerous variants | Bash(git checkout) blocks both git checkout -b new-branch (safe) and git checkout -- file (dangerous) |
| Can't parse flags semantically | Bash(rm -rf) blocks rm -rf /tmp/cache (safe) but allows rm -r -f / (dangerous, different flag order) |
| Can't detect shell wrappers | sh -c "rm -rf /" bypasses a Bash(rm) deny rule entirely |
| Can't analyze interpreter one-liners | python -c 'os.system("rm -rf /")' executes without matching any rm rule |
This hook provides semantic command analysis: it parses arguments, understands flag combinations, recursively analyzes shell wrappers, and distinguishes safe operations (temp directories, within cwd) from dangerous ones.
/plugin marketplace add kenryu42/cc-marketplace
/plugin install safety-net@cc-marketplaceNote
After installing the plugin, you need to restart your Claude Code for it to take effect.
- Run
/plugin→ SelectMarketplaces→ Choosecc-marketplace→ Enable auto-update
| Command Pattern | Why It's Dangerous |
|---|---|
| git checkout -- files | Discards uncommitted changes permanently |
| git checkout <ref> -- <path> | Overwrites working tree with ref version |
| git restore files | Discards uncommitted changes |
| git restore --worktree | Explicitly discards working tree changes |
| git reset --hard | Destroys all uncommitted changes |
| git reset --merge | Can lose uncommitted changes |
| git clean -f | Removes untracked files permanently |
| git push --force / -f | Destroys remote history |
| git branch -D | Force-deletes branch without merge check |
| git stash drop | Permanently deletes stashed changes |
| git stash clear | Deletes ALL stashed changes |
| git worktree remove --force | Force-deletes worktree without checking for changes |
| rm -rf (paths outside cwd) | Recursive file deletion outside the current directory |
| rm -rf / or ~ or $HOME | Root/home deletion is extremely dangerous |
| find ... -delete | Permanently removes files matching criteria |
| xargs rm -rf | Dynamic input makes targets unpredictable |
| xargs <shell> -c | Can execute arbitrary commands |
| parallel rm -rf | Dynamic input makes targets unpredictable |
| parallel <shell> -c | Can execute arbitrary commands |
| Command Pattern | Why It's Safe |
|---|---|
| git checkout -b branch | Creates new branch |
| git checkout --orphan | Creates orphan branch |
| git restore --staged | Only unstages, doesn't discard |
| git restore --help/--version | Help/version output |
| git branch -d | Safe delete with merge check |
| git clean -n / --dry-run | Preview only |
| git push --force-with-lease | Safe force push |
| rm -rf /tmp/... | Temp directories are ephemeral |
| rm -rf /var/tmp/... | System temp directory |
| rm -rf $TMPDIR/... | User's temp directory |
| rm -rf ./... (within cwd) | Limited to current working directory |
When a destructive command is detected, the plugin blocks the tool execution and provides a reason.
Example output:
BLOCKED by Safety Net
Reason: git checkout -- discards uncommitted changes permanently. Use 'git stash' first.
Command: git checkout -- src/main.py
If this operation is truly needed, ask the user for explicit permission and have them run the command manually.
You can manually test the hook by attempting to run blocked commands in Claude Code:
# This should be blocked
git checkout -- README.md
# This should be allowed
git checkout -b test-branchjust setup
# or
uv sync && uv run pre-commit install.claude-plugin/
plugin.json
marketplace.json
hooks/
hooks.json
scripts/
safety_net.py # Entry point
safety_net_impl/
__init__.py
hook.py # Main hook logic
rules_git.py # Git command rules
rules_rm.py # rm command rules
shell.py # Shell parsing utilities
tests/
safety_net_test_base.py
test_safety_net_audit.py
test_safety_net_edge.py
test_safety_net_find.py
test_safety_net_git.py
test_safety_net_parsing_helpers.py
test_safety_net_rm.py
By default, unparseable commands are allowed through. Enable strict mode to fail-closed
when the hook input or shell command cannot be safely analyzed (e.g., invalid JSON,
unterminated quotes, malformed bash -c wrappers):
export SAFETY_NET_STRICT=1Paranoid mode enables stricter safety checks that may be disruptive to normal workflows. You can enable it globally or via focused toggles:
# Enable all paranoid checks
export SAFETY_NET_PARANOID=1
# Or enable specific paranoid checks
export SAFETY_NET_PARANOID_RM=1
export SAFETY_NET_PARANOID_INTERPRETERS=1Paranoid behavior:
- rm: blocks non-temp
rm -rfeven within the current working directory. - interpreters: blocks interpreter one-liners like
python -c,node -e,ruby -e, andperl -e(these can hide destructive commands).
The guard recursively analyzes commands wrapped in shells:
bash -c 'git reset --hard' # Blocked
sh -lc 'rm -rf /' # BlockedDetects destructive commands hidden in Python/Node/Ruby/Perl one-liners:
python -c 'import os; os.system("rm -rf /")' # BlockedBlock messages automatically redact sensitive data (tokens, passwords, API keys) to prevent leaking secrets in logs.
All blocked commands are logged to ~/.cc-safety-net/logs/<session_id>.jsonl for audit purposes:
{"ts": "2025-01-15T10:30:00Z", "command": "git reset --hard", "segment": "git reset --hard", "reason": "...", "cwd": "/path/to/project"}Sensitive data in log entries is automatically redacted.
MIT