I was reviewing GitHub Issues this week and noticed something odd: three of the most-reacted issues (186 reactions combined) are all the same underlying problem — Claude Code fighting its own design.
Claude has built-in tools (Read, Edit, Grep, Glob) that are faster and safer than bash equivalents. But it keeps reaching for sed, grep, and cat anyway. And that preference causes a cascade of problems.
Here are three hooks that fix it. Each is under 20 lines.
1. The Bash Addiction (Issue #19649, 48 reactions)
The problem: Claude uses sed -n '10,20p' instead of the Read tool. It runs grep -r "pattern" instead of the built-in Grep. It creates files with cat <<EOF instead of Write. Every one of these triggers an extra permission prompt that can't be cached.
Why it happens: LLM training data is full of bash one-liners. Claude defaults to what it "knows" from Stack Overflow, not what it has available.
The fix: A PreToolUse hook that intercepts Bash commands containing these patterns and denies them with a pointer to the correct built-in tool.
#!/bin/bash
# ~/.claude/hooks/enforce-builtin-tools.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
# Check all segments of piped/chained commands
while IFS= read -r segment; do
cmd=$(echo "$segment" | sed 's/^[[:space:]]*//' | sed 's/^[A-Za-z_][A-Za-z_0-9]*=[^ ]* //')
base=$(basename "$(echo "$cmd" | awk '{print $1}')" 2>/dev/null)
case "$base" in
cat) msg="Use Read to read files, or Write to create them" ;;
head|tail) msg="Use Read with offset/limit parameters" ;;
sed) msg="Use Edit for modifications, Read for line ranges" ;;
grep|rg) msg="Use the built-in Grep tool" ;;
find) msg="Use the built-in Glob tool" ;;
*) continue ;;
esac
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Do not use \`$base\`. $msg\"}}"
exit 0
done < <(echo "$COMMAND" | tr '|' '\n' | sed 's/[;&]\{1,2\}/\n/g')
exit 0
This catches grep even after a pipe (cmd | grep) or in chained commands.
2. The cd Trap (Issue #28240, 90 reactions)
The problem: Claude runs cd /some/dir && npm install. The permission prompt shows cd:* which can't be whitelisted. The actual dangerous command (npm install) is hidden behind the harmless cd.
This is the most-reacted Claude Code issue of this type — 90 reactions. The permission system evaluates the first command in the chain, not the one that matters.
The fix: A hook that blocks cd chaining and feeds back the real command, so Claude retries immediately without splitting into three separate tool calls.
#!/bin/bash
# ~/.claude/hooks/no-cd-chaining.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE '^\s*(cd|pushd)\s+\S+\s*(&&|;|\|\|)'; then
REAL_CMD=$(echo "$COMMAND" | sed -E 's/^\s*(cd|pushd)\s+("[^"]*"|'\''[^'\'']*'\''|[^ &;|]+)\s*(&&|;|\|\|)\s*//')
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Do not prefix with cd. Run directly: $REAL_CMD\"}}"
exit 0
fi
exit 0
Pair this with "permissions": { "allow": ["Bash(cd:*)"] } as a safety net. cd alone doesn't mutate anything.
3. Plan Mode Escape (Issue #14259, 48 reactions)
The problem: Claude exits plan mode and starts implementing before the plan is reviewed. There's no PrePlanMode or PostPlanMode hook event, so users can't control when planning transitions happen.
The fix: While dedicated plan mode events don't exist yet, PostToolUse + ExitPlanMode works:
#!/usr/bin/env bash
# ~/.claude/hooks/archive-plan.sh
# Archives the plan when Claude exits plan mode
PLAN_FILE=$(ls -t "${HOME}/.claude/plans"/*.md 2>/dev/null | head -1)
if [[ -n "$PLAN_FILE" && -f "$PLAN_FILE" ]]; then
ARCHIVE="${CLAUDE_PROJECT_DIR:-.}/.plans/archive"
mkdir -p "$ARCHIVE"
cp "$PLAN_FILE" "$ARCHIVE/$(basename "$PLAN_FILE" .md)-$(date +%Y%m%d-%H%M%S).md"
fi
exit 0
{
"hooks": {
"PostToolUse": [{
"matcher": "ExitPlanMode",
"hooks": [{"type": "command", "command": "bash ~/.claude/hooks/archive-plan.sh"}]
}]
}
}
For blocking plan exit (not just archiving), use PermissionRequest + ExitPlanMode instead — exit code 2 blocks the transition, and your reason is sent back to Claude.
The Pattern
All three issues share one root cause: Claude Code's default behaviors conflict with its own architecture. It has built-in tools but prefers bash. It chains commands that break permissions. It exits plan mode without checkpoint.
The fix is always the same: a PreToolUse or PostToolUse hook that enforces the behavior the architecture intended.
These hooks are running in production right now — 700+ hours of autonomous Claude Code operation. The issues they solve are real, and 186 reactions prove a lot of people hit them.
Check if your setup has these gaps: npx cc-health-check — free, 20 checks, nothing leaves your machine.
Top comments (0)