Fix: Claude Code PreToolUse Hooks Stop Working

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Error

You have Claude Code running with --dangerously-skip-permissions and a PreToolUse hook configured to log and allow all tool calls. Everything works initially. Then, after a background task completes (or after an extended idle period), permission prompts start appearing:

Permission rule **Bash** requires confirmation for this command

Your hook log shows no entry for the blocked command — the hook was never called. All subsequent tool calls in the session prompt for permission.

Quick Fix

# Kill and restart Claude Code
# Ctrl+C to exit the broken session

# Restart with the same flags
claude --dangerously-skip-permissions --permission-mode bypassPermissions

There is no way to recover the permission state within the same session once it degrades.

What’s Happening

Claude Code maintains an internal permission state machine that tracks which permission mode is active and whether hooks should be invoked for each tool call. This state machine has a bug where certain events cause it to reset to the default (interactive confirmation) mode:

Trigger events that cause permission state degradation:

  1. Background task completion — When a Bash tool call with run_in_background: true completes, the permission state handler processes the completion event and can reset the session’s permission configuration
  2. Extended idle periods — Sessions left idle for 30+ minutes may experience the same reset
  3. Subagent spawning — Child agent processes may not inherit the parent’s permission configuration

Evidence from hook logs:

# Hook is being called normally:
2026-04-14 15:53:33 tool=Bash decision=allow input_bytes=951 elapsed_ms=23
2026-04-14 15:53:39 tool=Bash decision=allow input_bytes=849 elapsed_ms=23
2026-04-14 15:58:35 tool=Agent decision=allow input_bytes=1203 elapsed_ms=24
2026-04-14 16:00:39 tool=Bash decision=allow input_bytes=427 elapsed_ms=21
2026-04-14 16:01:01 tool=Bash decision=allow input_bytes=484 elapsed_ms=17
# ^^^ last hook entry before the blocked command
# The blocked command has NO log entry — hook was never invoked

The hook was not just returning a different decision — it was never called at all. This confirms the issue is in the permission dispatch layer, not in the hook execution.

Settings that do NOT prevent the issue:

{
  "defaultMode": "bypassPermissions",
  "skipDangerousModePermissionPrompt": true,
  "sandbox": {
    "autoAllowBashIfSandboxed": true
  }
}

All of these are correctly configured but none prevents the permission state from degrading.

Step-by-Step Solution

1. Detect the Problem Early

Add monitoring to your hook script to alert you when it stops being called:

#!/bin/bash
# ~/.claude/hooks/monitored-allow.sh

LOG_FILE="$HOME/.claude/hook-activity.log"
TOOL_NAME="${CLAUDE_HOOK_TOOL_NAME:-unknown}"

# Log every invocation with timestamp
echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') tool=$TOOL_NAME" >> "$LOG_FILE"

# Return allow
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'

Then set up a watcher:

# In a separate terminal, watch for gaps in hook activity
tail -f ~/.claude/hook-activity.log

If you see permission prompts but no new log entries, the hook has been bypassed.

2. Avoid Background Tasks in Permission-Critical Sessions

If you need reliable --dangerously-skip-permissions behavior:

# Instead of background tasks within Claude Code,
# run long-running commands in a separate terminal

# DON'T do this in Claude Code:
# "Run this build in the background"

# DO ask Claude to give you the command, then run it yourself:
# "What command should I run to build this project?"

3. Use Session Watchdog Script

#!/bin/bash
# claude-watchdog.sh — restarts Claude Code if hooks stop firing

HOOK_LOG="$HOME/.claude/hook-activity.log"
MAX_SILENCE_SECONDS=300  # 5 minutes

while true; do
    if [ -f "$HOOK_LOG" ]; then
        last_entry=$(tail -1 "$HOOK_LOG")
        last_ts=$(echo "$last_entry" | cut -d' ' -f1)
        last_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$last_ts" "+%s" 2>/dev/null)
        now_epoch=$(date "+%s")

        if [ -n "$last_epoch" ]; then
            silence=$((now_epoch - last_epoch))
            if [ "$silence" -gt "$MAX_SILENCE_SECONDS" ]; then
                echo "WARNING: No hook activity for ${silence}s"
            fi
        fi
    fi
    sleep 60
done

4. Register the Hook Correctly

Ensure your hook configuration in ~/.claude/settings.json uses a broad matcher:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": ".*",
      "hooks": [{
        "type": "command",
        "command": "/absolute/path/to/hook.sh",
        "timeout": 30
      }]
    }]
  }
}

Key points:

Prevention

Tools That Help

For developers running Claude Code in automation pipelines where permission integrity is critical, a dev tool extension can help monitor and debug the tool call flow in real time.