Preventing Claude Code from Running Dangerous Commands: Hook Strategies and CLAUDE.md
Claude Code can automate code edits, run commands, and modify files, but its powerful capabilities risk unintended actions; this article explains how to use Hooks—configurable event handlers such as Notification, PostToolUse, and PreToolUse—to enforce formatting, block risky commands, and require permission checks, ensuring safe and reliable AI‑assisted development.
What Hooks Are
Hooks are fixed checkpoints in Claude Code’s workflow. When a hook fires, Claude passes a JSON payload describing the event to a handler. The handler can be one of five types:
command – runs a shell command on the local machine.
http – POSTs the JSON to a remote HTTP endpoint.
mcp_tool – invokes a tool that is already connected to an MCP server.
prompt – asks the LLM for a yes/no style decision.
agent – runs a sub‑agent with tool‑access capabilities.
Each hook definition consists of an event name , a matcher that filters which tool invocations trigger the hook, and the handler configuration.
Typical Hook Scenarios
Notification – low‑risk alert
Show a desktop notification whenever Claude needs user attention (e.g., a permission prompt). The matcher "permission_prompt" limits the hook to permission‑request events.
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}PostToolUse – automatic formatting
After a successful Edit or Write operation, run a formatter (e.g., Prettier) on the edited file. The matcher "Edit|Write" ensures the hook only runs for file‑editing tools.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}PreToolUse – guard dangerous commands and sensitive files
Run before any tool execution. The script parses the incoming JSON, checks tool_name, command, and file_path, and aborts with exit 2 if a risky pattern (e.g., rm -rf, chmod -R 777) or a protected path ( .env, .git, private keys) is detected.
#!/usr/bin/env bash
set -euo pipefail
input="$(cat)"
tool=$(echo "$input" | jq -r '.tool_name // empty')
command=$(echo "$input" | jq -r '.tool_input.command // empty')
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ "$tool" == "Bash" ]] && [[ "$command" =~ rm[[:space:]]+-rf|chmod[[:space:]]+-R[[:space:]]+777 ]]; then
echo "Blocked risky command: $command" >&2
exit 2
fi
if [[ "$tool" == "Edit" || "$tool" == "Write" ]]; then
case "$file" in
*.env|*.env.*|*/.env|*/.git/*|*id_rsa*|*id_ed25519*)
echo "Blocked sensitive file edit: $file" >&2
exit 2
;;
esac
fi
exit 0Save the script (e.g., .claude/hooks/guard.sh), make it executable, and reference it in the PreToolUse configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh"
}
]
}
]
}
}Choosing the Right Handler
command– deterministic, runs locally; ideal for formatting, notifications, and simple security checks. http – send the JSON to a remote service; useful for centralized audit logs or policy servers. mcp_tool – call an already‑connected MCP tool; best when an organization provides a managed capability. prompt and agent – involve the LLM for decisions that cannot be expressed deterministically; use only when a rule truly requires model reasoning.
Lifecycle Events Overview
SessionStart – session begins or is restored.
UserPromptSubmit – after the user submits a prompt, before Claude processes it.
PreToolUse – before a tool (Bash, Edit, Write, etc.) runs.
PermissionRequest – when a tool needs explicit permission.
PostToolUse – after a tool succeeds.
Stop – Claude finishes a response round.
PreCompact – before context compression.
Hooks vs. Skills
Hooks are automatically triggered at fixed lifecycle points and are meant for deterministic, low‑risk actions (formatting, security checks, notifications). Skills are on‑demand knowledge bundles ( SKILL.md) that Claude loads only when needed, suitable for complex workflows, code reviews, or multi‑step reasoning.
Practical Roll‑out Strategy
Start with a Notification hook to confirm the configuration loads correctly.
Add a PostToolUse formatter for your project (Prettier, Ruff, spotlessApply, etc.).
Enable the PreToolUse guard to block high‑risk commands and protect sensitive paths.
Enable hooks gradually; each additional hook adds complexity and can make debugging harder. Use the /hooks command in Claude Code to inspect which hooks are active, their matchers, and handlers. If a hook misbehaves, isolate it by disabling others, run the script manually with sample JSON, and check exit codes ( exit 2 for blocking, exit 0 for success).
Troubleshooting Checklist
Is the settings file in the correct location ( ~/.claude/settings.json, .claude/settings.json, or .claude/settings.local.json) and valid JSON?
Does the matcher actually match the incoming event (check with /hooks)?
Does the script read from stdin and output only JSON on stdout (debug info must go to stderr)?
Use exit 2 for blocking; exit 1 is treated as a non‑blocking error.
For permission‑related hooks, prefer PreToolUse in non‑interactive mode; PermissionRequest only works when a UI prompt can be shown.
Hook Input / Output Details
When a hook fires, Claude sends the event JSON to the handler:
If the handler type is command, the JSON is provided on stdin.
If the handler type is http, the JSON is sent as the POST body.
Common fields in every event: session_id – current session identifier. transcript_path – path to the session JSONL file. cwd – working directory at the moment of the hook. hook_event_name – name of the event that triggered the hook.
Tool‑related events also contain: tool_name – e.g., Bash, Edit, Write. tool_input – object with fields such as command or file_path.
If a hook returns JSON on stdout, Claude will parse it according to the hook schema. For blocking hooks, return exit 2 and optionally write a human‑readable reason to stderr. An exit 0 with valid JSON is interpreted as a decision (e.g., allow, deny, ask).
Important Behaviour Notes
Only exit 2 can stop a tool invocation; exit 1 is a non‑blocking error for most events.
If multiple hooks match the same event, they run in parallel and their results are merged. A deny from one hook does not prevent other hooks from logging or sending HTTP requests, but the final decision follows the most restrictive outcome.
Hooks can modify the tool input, but when several hooks try to change the same field the last one to finish wins – the order is nondeterministic. command hooks execute with the current user’s permissions, so they can read, modify, or delete any file the user can access. Review scripts carefully before adding them.
Common Pitfalls
Using exit 1 for blocking – the hook will not stop the tool.
Writing debug logs to stdout – Claude will try to parse the output as JSON and may fail.
Matcher that is too broad (e.g., .*) – causes the hook to run on every tool call, leading to performance issues.
Relying on a single black‑list entry for dangerous commands; attackers can bypass it with equivalent syntax. Combine command patterns, path restrictions, sandboxing, CI checks, and manual review.
How to Debug a Misbehaving Hook
Run /hooks inside Claude Code to verify that the hook is loaded and attached to the expected event.
Execute the script manually by feeding it a sample JSON payload. Example for the guard script:
printf '%s
' '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/demo"}}' | .claude/hooks/guard.sh
echo $?The expected exit code is 2 for a blocked command.
If the script works in isolation, add temporary stderr logging inside the script to capture the exact JSON Claude passes.
echo "$input" >> /tmp/claude-hook-debug.logAfter reproducing the issue, inspect the log to ensure the matcher and fields are as expected.
Enable only one hook at a time while debugging; parallel execution can obscure which hook caused the problem.
Summary
Hooks let you inject deterministic actions at key points of Claude Code’s execution:
Notification – alert the user when Claude needs attention.
PostToolUse – run a formatter or logger after a successful tool call.
PreToolUse – block dangerous commands and protect sensitive files before they are executed.
Start with these three hooks, verify they work, then expand only as needed. Keep matchers as narrow as possible, use exit 2 for hard blocks, and always send diagnostic output to stderr. Hooks provide reliable safety nets, but sandboxing, permission policies, CI checks, and manual review remain essential for comprehensive security.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
