feat: add missing pre-tool-check.sh hook to 06-hooks (#40)

* feat: add missing pre-tool-check.sh hook to 06-hooks

The LEARNING-ROADMAP.md (Milestone 2A) referenced this file in an
exercise that copies it to ~/.claude/hooks/, but the file did not exist.
This caused confusion for learners following the guide.

The new pre-tool-check.sh is a PreToolUse hook for the Bash matcher that:
- Blocks unconditionally destructive commands (rm -rf /, dd, fork bomb, etc.)
- Warns on high-risk commands (rm -rf, git push --force, DROP TABLE, etc.)
- Reads tool input JSON from stdin (matching Claude Code hook protocol)
- Requires no external dependencies (pure bash + grep)

Fixes #32

* fix(hooks): correct exit code, remove set -e, use portable sed in pre-tool-check.sh

- Change `exit 1` to `exit 2` so Claude Code actually blocks the command
  (exit 1 is treated as a non-blocking error; exit 2 is required to block)
- Remove `set -euo pipefail`: `set -e` caused the script to exit on the
  first non-matching grep result, skipping all remaining pattern checks
- Replace non-portable `grep -o '"command"\s*:\s*"[^"]*"'` with
  `sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'`
  which works on both macOS (BSD) and Linux without GNU grep extensions

Closes #32

---------

Co-authored-by: Luong NGUYEN <luongnv89@gmail.com>
This commit is contained in:
JiayuWang(王嘉宇)
2026-04-07 07:18:12 +08:00
committed by GitHub
parent 699fb39a46
commit b511ed1997
+96
View File
@@ -0,0 +1,96 @@
#!/bin/bash
# Pre-tool safety check for Bash commands
# Hook: PreToolUse (matcher: Bash)
#
# This hook runs before every Bash tool execution and blocks or warns on
# potentially destructive or high-risk shell commands.
#
# Setup:
# cp 06-hooks/pre-tool-check.sh ~/.claude/hooks/
# chmod +x ~/.claude/hooks/pre-tool-check.sh
#
# Configure in ~/.claude/settings.json:
# {
# "hooks": {
# "PreToolUse": [
# {
# "matcher": "Bash",
# "hooks": [
# {
# "type": "command",
# "command": "~/.claude/hooks/pre-tool-check.sh"
# }
# ]
# }
# ]
# }
# }
#
# Input: JSON via stdin with the shape:
# { "tool_name": "Bash", "tool_input": { "command": "..." } }
#
# Output: Exit 0 to allow, exit 2 to block, or print JSON to modify behavior.
# Read the full JSON input from stdin
INPUT=$(cat)
# Extract the command using portable sed (compatible with macOS and Linux)
COMMAND=$(echo "$INPUT" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
# Fall back to the raw input if extraction fails
if [ -z "$COMMAND" ]; then
COMMAND="$INPUT"
fi
# ── Blocked patterns ──────────────────────────────────────────────────────────
# These commands are blocked unconditionally because they are almost always
# destructive and rarely intentional in an automated context.
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf \*"
"dd if=/dev/zero"
"dd if=/dev/random"
":(){:|:&};:" # Fork bomb
"mkfs\." # Filesystem format
"format c:" # Windows disk format
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$pattern"; then
echo "❌ Blocked: Potentially destructive command detected: $pattern"
echo " Command: $COMMAND"
exit 2
fi
done
# ── Warning patterns ──────────────────────────────────────────────────────────
# These patterns are risky but may be intentional. Log a warning and allow.
WARNING_PATTERNS=(
"rm -rf"
"git push --force"
"git reset --hard"
"git clean -f"
"chmod -R 777"
"sudo rm"
"DROP TABLE"
"DROP DATABASE"
"truncate"
)
WARNINGS=0
for pattern in "${WARNING_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "⚠️ Warning: High-risk operation detected: $pattern"
WARNINGS=$((WARNINGS + 1))
fi
done
if [ "$WARNINGS" -gt 0 ]; then
echo " Command: $COMMAND"
echo " Proceeding — review the above warnings before continuing."
fi
# ── Allow ─────────────────────────────────────────────────────────────────────
exit 0