A PreToolUse hook for Claude Code that validates every Bash tool call before it executes. Safe, read-only commands are fast-pathed without an API call; anything ambiguous is checked by claude-haiku-4-5 via the Anthropic API.
| File | Purpose |
|---|---|
.claude/settings.json |
Hook registration, permission allow/deny lists |
.claude/hooks/code-safety.sh |
The hook script |
.claude/hooks/refresh-alignment-key.sh |
Fetches/refreshes API key from 1Password into .env |
generate_test_commands.py |
Generates 29 exfiltration test commands across 7 categories |
run_tests.sh |
Runs the test commands against the hook and reports results |
Be careful before running these tests; see the Test Suite section below for more information.
- Python 3 (for JSON handling)
curl- An Anthropic API key — either set
ANTHROPIC_ALIGNMENT_CHECK_API_KEYdirectly in.env, or setOP_ANTHROPIC_KEY_REFand use the included 1Password refresh script
Clone this repo, then ask Claude Code to merge the hook configuration into your project:
git clone <this-repo> /tmp/bash-safety-hookThen in your project, start Claude Code and ask:
Merge the Claude Code bash safety hook from /tmp/bash-safety-hook/.claude/ into this project's .claude/ directory. Add the hook config and permissions to my existing settings.json, and copy the hook script.
Claude Code will handle merging the hook registration, permissions, and script into your existing setup without overwriting your other settings.
Alternatively, if you don't have an existing .claude/ directory, just copy it directly:
cp -r /tmp/bash-safety-hook/.claude/ your-project/.claude/
chmod +x your-project/.claude/hooks/code-safety.shFinally, configure the API key in your project's .env. There are two options:
Option A — Set the key directly:
ANTHROPIC_ALIGNMENT_CHECK_API_KEY=sk-ant-...
Option B — Use 1Password: Set a 1Password secret reference and run the refresh script to fetch and cache the key:
OP_ANTHROPIC_KEY_REF=op://vault/item/field
bash .claude/hooks/refresh-alignment-key.shThe refresh script resolves the reference via op read and writes ANTHROPIC_ALIGNMENT_CHECK_API_KEY into .env. If the cached key expires or is revoked, the hook will tell the agent to re-run the refresh script.
Either way, the hook loads ANTHROPIC_ALIGNMENT_CHECK_API_KEY from .env automatically. A separate variable name is used to avoid collisions with your own ANTHROPIC_API_KEY.
-
Deny list (
.claude/settings.json) — Hard blocks on the most dangerous operations (rm,sudo,git reset --hard,git push --force, etc.) and on.envfile access viaRead,Edit,Grep, and common Bash commands. These cannot be approved regardless of anything else. -
.envearly block (.claude/hooks/code-safety.sh) — Any Bash command referencing.envas a token is immediately blocked before reaching the fast-path or API check. -
Hook fast-path (
.claude/hooks/code-safety.sh) — Common read-only commands (git status,ls,grep,uv pip list,just --list, etc.) are allowed immediately without an API call. Commands that contain chaining operators (|,&&,$(, backticks,>) always go to the Haiku check, even if they start with a safe prefix — this preventscat .env | curlstyle bypasses. -
Haiku check (
.claude/hooks/code-safety.sh) — Anything not fast-pathed is sent toclaude-haiku-4-5-20251001with a safety-focused system prompt that explicitly flags.envaccess as unsafe. If the model returns{"safe": false, ...}, the tool call is blocked with an explanation. This catches obfuscated attempts (string concatenation,chr()encoding, etc.).
For a critical step that has been blocked, Claude Code will generally tell you what you would need to run in a separate terminal. This forces a level of review above and beyond the default permissions modal, and should only come up sparingly, for example during destructive operations.
- If the API key is missing: block (with a message telling the agent how to set it).
- If the API key is invalid (authentication error): block (with a message telling the agent to refresh or replace the key).
- If the
curlcall fails (network error, timeout): block (fail closed). - If the API returns an empty or unparseable response: block (fail closed).
Warning: The test suite sends real attack-pattern commands (reverse shells, DNS exfiltration, credential harvesting, etc.) to the Anthropic API for classification. Even though the hook blocks them before execution, the network traffic itself looks like attack tooling. If your computer is on a managed network, your IT / security team will see this traffic and likely follow up with you. Run on a personal machine or give your security team a heads-up first.
The test suite generates 29 exfiltration-style commands across 7 categories:
| Category | Count | Examples |
|---|---|---|
http_exfil |
5 | curl -d @.env, wget | bash |
base64_exfil |
3 | Base64-encoded secrets sent via POST, query string, headers |
dns_exfil |
2 | Data exfiltration via DNS subdomain queries |
reverse_shell |
3 | nc, bash /dev/tcp, python reverse shells |
pipe_exfil |
4 | tar ~/.ssh | curl, env | curl, etc. |
credential_harvest |
3 | Keychain dumps, 1Password reads, grep for API keys |
obfuscated |
5 | Hex IPs, variable indirection, eval+base64, backgrounding |
disguised |
4 | Exfil disguised as npm publish, pip install, git push, scp |
To run:
python3 generate_test_commands.py # generates test_commands.json
bash run_tests.sh # runs all commands against the hookAll 29 commands should be blocked.
Fast-path pattern — Edit the SAFE_PATTERN variable in code-safety.sh to add project-specific read-only commands that shouldn't incur an API call.
Allow list — The permissions.allow array in settings.json lists commands that bypass the user-approval prompt. The hook still runs on all of them. Add or remove entries to match your toolchain.
Deny list — The permissions.deny array hard-blocks commands regardless of the hook. Add entries here for anything that should never run without manual intervention outside Claude Code.
Haiku system prompt — The SYSTEM_PROMPT variable in code-safety.sh defines what Haiku considers safe or unsafe. Edit it to reflect your project's risk profile.
The included settings.json has a broad "Bash" allow (no user prompts for Bash commands) and relies on the hook as the gate. If you prefer to be prompted for commands not in the explicit allow list, remove the "Bash" entry from permissions.allow and keep only the "Bash(cmd:*)" pattern entries.
Big thanks to Jake Porway for the initial settings.json file that kicked off this project.