From b511ed1997bbaa356e76da78ca7152f96370c70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JiayuWang=28=E7=8E=8B=E5=98=89=E5=AE=87=29?= <151589547+JiayuuWang@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:18:12 +0800 Subject: [PATCH] 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 --- 06-hooks/pre-tool-check.sh | 96 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 06-hooks/pre-tool-check.sh diff --git a/06-hooks/pre-tool-check.sh b/06-hooks/pre-tool-check.sh new file mode 100644 index 0000000..932395e --- /dev/null +++ b/06-hooks/pre-tool-check.sh @@ -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