A guard hook for Claude Code that blocks dangerous tool calls before they run — rm -rf /, DROP TABLE, curl | bash, force-pushes to main, writes to .env, and more.
AI coding agents can autonomously execute shell commands, file operations, and database queries. Without guardrails, a misinterpreted instruction can cause irreversible damage. toolwarden hooks into Claude Code's PreToolUse event and scans every Bash, Write, and Edit call against a curated set of threat patterns:
| Category | Examples |
|---|---|
| Destructive file ops | rm -rf /, deleting .env or .git |
| Destructive git ops | git push --force to main/master, git reset --hard, git clean -f |
| Database destruction | DROP TABLE, TRUNCATE, DELETE FROM without WHERE |
| Permission abuse | chmod 777, recursive world-writable permissions |
| Network exfiltration | curl | bash, uploading files via curl -d @ / -T / -F |
| System danger | sudo, npm publish |
| Data loss | docker compose down -v, docker volume rm, rm on SQLite files |
| Protected paths | writes to .env, .git/ internals, key/credential/token files |
Rules are tiered:
- deny — irreversible or exfiltration patterns are blocked outright. The denial reason, including a safer alternative, is fed back to the agent so it can self-correct.
- ask — risky-but-sometimes-legitimate patterns (
git reset --hard,docker system prune, …) escalate to a permission prompt for per-case approval.
Zero dependencies: a single Python 3 stdlib script plus a JSON rules file. No network calls.
Clone the repo, then run the installer against any repository you want to protect:
git clone https://github.com/pazsitz/toolwarden.git
python3 toolwarden/install.py /path/to/your/repo # defaults to the current directoryThere is deliberately no curl | bash one-liner — toolwarden blocks that pattern for a reason. Clone or download, read install.py (it's short), then run it.
The installer:
- copies
toolwarden.pyandtoolwarden-patterns.jsoninto<repo>/.claude/hooks/, - registers the hook in
<repo>/.claude/settings.json(created if missing; existing settings are preserved; re-running never duplicates the entry).
Restart your Claude Code session afterwards — hooks are loaded at session start. Since the files live inside your repo, committing them means every teammate gets the same protection automatically.
Add the log directory to your repo's .gitignore:
.claude/logs/
Pull the latest toolwarden and re-run the installer. It overwrites the hook and core patterns, and never touches your local rules file.
python3 toolwarden/install.py /path/to/your/repo --uninstallRemoves the hook files and the settings entry; local rules and logs are left in place.
| Env var | Values | Effect |
|---|---|---|
TOOLWARDEN_MODE |
enforce (default) / warn |
warn never blocks — logs the match and surfaces a warning |
TOOLWARDEN_ALLOWLIST |
comma-separated rule IDs | Skips those rules, e.g. TOOLWARDEN_ALLOWLIST=sudo,git-reset-hard |
The hook inherits its environment from the Claude Code session, so set these either in the shell you launch Claude Code from, or per project via the env key in .claude/settings.json:
{ "env": { "TOOLWARDEN_ALLOWLIST": "sudo" } }Add your own rules in .claude/hooks/toolwarden-patterns.local.json (next to the installed hook). Local rules are evaluated before core rules, and a local rule with the same id replaces the core rule — so you can also retier or reword a built-in. Updates never overwrite this file.
Each rule:
{
"id": "rm-user-data",
"category": "project-data",
"tier": "deny",
"tools": ["Bash"],
"regex": "\\brm\\s[^|;&]*\\bdata/users(/|\\b)",
"message": "rm targeting data/users/ — the primary user data store of this project.",
"alternative": "Move to a backup location instead: mv data/users/<x> /tmp/backup-<x>"
}See examples/toolwarden-patterns.local.json for a complete example. Within each file, rules are evaluated in order and the first match wins — keep deny rules before ask rules.
| ID | Tier | Catches |
|---|---|---|
rm-rf-root |
deny | recursive rm on /, ~, or $HOME |
rm-env-git |
deny | deleting .env or .git |
sql-drop |
deny | DROP TABLE/DATABASE/SCHEMA |
sql-truncate |
deny | SQL TRUNCATE (file truncate -s is fine) |
sql-delete-no-where |
deny | DELETE FROM without WHERE |
curl-pipe-shell |
deny | curl/wget piped into a shell |
curl-upload |
deny | curl uploading local files (-d @, -T, -F …=@) |
chmod-777-recursive |
deny | recursive world-writable permissions |
git-force-push-main |
deny | force-push to main/master (--force-with-lease allowed) |
sudo |
deny | any sudo command |
npm-publish |
deny | publishing to the npm registry |
docker-down-volumes |
deny | docker compose down -v (deletes volumes) |
docker-volume-rm |
deny | docker volume rm / prune |
rm-database-files |
deny | rm on .db / .sqlite files |
write-env |
deny | Write/Edit to .env* (except .env.example) |
write-git-internals |
deny | Write/Edit inside .git/ |
write-secrets |
deny | Write/Edit to .pem/.key/credentials/token files |
git-reset-hard |
ask | git reset --hard |
git-clean-force |
ask | git clean -f |
chmod-777-single |
ask | non-recursive chmod 777 |
rm-rf-subpath |
ask | recursive rm outside /tmp |
docker-system-prune |
ask | docker system prune |
Matched events and internal errors append JSON Lines to .claude/logs/toolwarden.jsonl:
{"ts": "2026-07-16T10:00:00+00:00", "tool": "Bash", "rule_id": "sudo", "category": "system-danger", "tier": "deny", "decision": "deny", "mode": "enforce", "target_snippet": "sudo apt install foo"}Clean (non-matching) calls are not logged.
- Fail-open: if the hook itself errors (malformed patterns file, unexpected exception), the tool call proceeds and a visible
systemMessagewarns that the guard is inactive; the error is logged. A broken guard should never brick your agent. - Regex scanning is defense-in-depth, not a sandbox — deliberately obfuscated commands can evade it. Claude Code's permission prompts remain the primary gate; toolwarden catches the accidental and the obvious.
pip install pytest
pytest -vThis repo dogfoods its own hook: .claude/settings.json points at src/toolwarden.py.
New rules are welcome — a rule PR should include the rule itself plus matching and near-miss test cases in tests/test_patterns.py.