Commit Graph

2 Commits

Author SHA1 Message Date
yarlinghe
bce7cf8cb4 fix(hooks): correct pre-tool-check.sh hook protocol bugs (#72)
The pre-tool-check.sh example had three bugs rooted in the same
misunderstanding of the Claude Code PreToolUse hook protocol
(stdin/stdout/stderr + exit code contract):

1. Substring matching on `rm -rf /`
   The pattern was unanchored, so grep treated it as a substring and
   falsely blocked any command containing `rm -rf /` — including benign
   calls like `rm -rf /tmp/build` or `rm -rf /var/cache/foo`. Fixed by
   anchoring the slash to a whitespace-or-end-of-line boundary.

2. WARN tier was dead code
   The warning layer printed to stderr and then `exit 0`. Claude Code
   silently discards stderr on exit 0, so the warnings were never seen
   by Claude, the user, or any log. Fixed by adding an audit log file
   at `$CLAUDE_PROJECT_DIR/.claude/hooks/audit.log` that records every
   invocation with its decision (BLOCK/WARN/ALLOW). The audit log is
   now the reliable observability mechanism for the WARN tier.

3. BLOCK reasons printed to stdout instead of stderr
   On `exit 2`, Claude Code reads stderr to surface the block reason
   to Claude. The echoes before `exit 2` defaulted to stdout, so
   Claude Code reported `"No stderr output"` and Claude had to read
   the hook source file to infer why a command was blocked. Fixed by
   explicitly redirecting the block-reason echoes to stderr with `>&2`.

Also escaped the regex metacharacters in the fork-bomb pattern
`:(){:|:&};:` so it matches literally under `grep -E`, and updated the
header docstring to document the stdout/stderr/exit-code convention so
future readers don't make the same mistakes.

Verified with 6 smoke tests covering: benign command (ALLOW), warn-tier
relative path, substring edge case (`rm -rf /tmp/...` no longer falsely
blocked), exact root match (`rm -rf /`, `rm -rf / ; echo` still blocked),
fork-bomb literal, and `git push --force` (WARN only). stdout is empty
in all cases; all reasons correctly routed to stderr or the audit log.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:48:05 +02:00
JiayuWang(王嘉宇)
b511ed1997 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>
2026-04-07 01:18:12 +02:00