PostToolUse hooks not firing despite settings.json wiring (Claude Code 2.1.119+, persistent ~42-batch chain)

Resolved 💬 3 comments Opened May 2, 2026 by dwynndev Closed May 6, 2026

Summary

Claude Code 2.1.119+ exhibits a GLOBAL PostToolUse hook layer regression where hooks wired in ~/.claude/settings.json do NOT fire on Edit / Write tool invocations despite:

  1. Valid settings.json wiring (verified via python3 -c "json.load(...)" + grep)
  2. Executable hook script (verified via ls -la + chmod +x)
  3. Functional smoke test in isolation (hook fires correctly when manually piped simulated PostToolUse JSON payload via stdin)

Regression affects ALL 8 PostToolUse hooks wired (verified empirically post-2.1.119 install). Pattern persists 42 consecutive sessions (April 24 → May 2, 2026) with zero hook firings despite continuous Tier 1 file edit activity.

Environment

  • Claude Code versions affected: 2.1.119 → 2.1.126 (persistent across version upgrades)
  • OS: macOS 13.x (Darwin 22.x)
  • Python: 3.x (system default)
  • Hook scripts wired: 8 PostToolUse hooks (auto-mirror-sync.py + instinct-observer.py + log-instructions-loaded.py + 5 others)
  • Settings.json wiring: verified valid via JSON parse + matcher + command path resolution

Reproduction

Step 1: Verify hook wiring is valid

python3 -c "
import json
d = json.load(open('$HOME/.claude/settings.json'))
posttooluse = d.get('hooks', {}).get('PostToolUse', [])
print(f'PostToolUse entries: {len(posttooluse)}')
for entry in posttooluse:
    matcher = entry.get('matcher', '')
    for hook in entry.get('hooks', []):
        cmd = hook.get('command', '')
        print(f'  matcher={matcher!r:30s} cmd={cmd}')
"

Expected: ≥1 entry with matcher matching Edit|Write (or similar) + executable command path.

Step 2: Verify hook script functional in isolation

HOOK=$HOME/.claude/hooks/auto-mirror-sync.py  # Pick any wired PostToolUse hook
ls -la "$HOOK"  # Confirms executable

# Simulated PostToolUse payload (matches Claude Code event JSON shape)
cat <<'EOF' | python3 "$HOOK"
{
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": {"file_path": "/tmp/test.md", "old_string": "a", "new_string": "b"},
  "tool_response": {"oldString": "a", "newString": "b"},
  "session_id": "test-session-id",
  "cwd": "/tmp"
}
EOF
echo "Exit code: $?"

Expected: exit 0 + log entry written to hook's designated log path. In our diagnostic: hook fires correctly in isolation, confirming script + wiring are valid.

Step 3: Trigger Edit on Tier 1 file via Claude Code session

In an active Claude Code session, execute an Edit operation on any file matching the hook's matcher. Example: edit ~/.claude/rules/security.md (matches Edit|Write matcher).

Step 4: Inspect hook log for firing evidence

LOG=/tmp/auto-mirror-sync.log  # Or whichever log path the hook writes to
ls -la "$LOG"
tail -20 "$LOG"

Expected per docs: log entry written within seconds of Edit completion.

Observed: log file shows entries from manual smoke tests only (Step 2). Zero entries from Claude-Code-triggered Edit operations across 42 consecutive sessions spanning ~9 days (April 24 → May 2, 2026).

Step 5: Confirm F197 PERSISTENT signature

# Inspect timestamp distribution in hook log
awk -F'\t' '{print $1}' "$LOG" | sort | uniq -c | tail -20

Expected: distribution of timestamps showing periodic firings during session activity.

Observed: timestamps cluster ONLY around manual smoke test invocations. Zero PostToolUse-triggered firings.

Diagnostic Evidence

Comprehensive diagnostic chain documented in three audit docs (~7,000 words combined):

  • v197 audit doc (3,711 words): Hypothesis A/B/C ruled-out via diagnostic instrumentation. auto-mirror-sync.py instrumented with +47L trace logging covering hook entry, payload parsing, settings.json validation, and exit-reason categories. Trace log shows zero hook entries during active session despite Edit activity. Hook script + wiring NOT the cause.
  • v201 escalation audit (~1,855 words): canonical workaround documentation + manual fallback procedure validation across 24-batch chain at codification time.
  • 42-batch persistence chain v177→v219 (April 24 → May 2, 2026): zero hook firings across continuous Tier 1 file editing activity. Each batch produces 5-10 mirror sync operations × 42 batches = ~210-420 mirror sync invocations, ALL handled via manual fallback with 100% SHA256 MATCH outcomes.

Workaround

Manual Python shutil.copy2 + SHA256 verification fallback (codified internally as v176 FR-1):

import shutil, hashlib, sys
from pathlib import Path

def mirror_sync_with_sha256(src, dst):
    """Manual fallback for PostToolUse hook regression (Claude Code 2.1.119+)."""
    src_p, dst_p = Path(src), Path(dst)
    if not src_p.exists():
        sys.exit(f"FAIL: source missing {src}")
    dst_p.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dst)
    src_hash = hashlib.sha256(src_p.read_bytes()).hexdigest()
    dst_hash = hashlib.sha256(dst_p.read_bytes()).hexdigest()
    if src_hash == dst_hash:
        print(f"MATCH: {src} → {dst} ({src_hash[:16]})")
    else:
        sys.exit(f"FAIL: SHA256 mismatch {src} vs {dst}")

Validation across 42-batch chain: 100% SHA256 MATCH outcomes; ~3-5 min overhead per batch (approximately 120-200 min cumulative across 42 batches). Operational continuity preserved; the regression is non-blocking but produces cumulative friction + observability gap (PostToolUse-driven instrumentation like instinct-observer.py records nothing).

Severity

MEDIUM — non-blocking (manual fallback works); operational continuity preserved. The user impact is:

  1. Cumulative friction: ~3-5 min/batch overhead for manual mirror sync × 42 batches = ~120-200 min cumulative
  2. Observability gap: instinct-observer.py PostToolUse hook (used for pattern observation across tool invocations) records zero entries → cognitive companion logging effectively disabled
  3. Hook ecosystem confidence: 8 PostToolUse hooks all silently non-functional → reduces trust in hook layer for new instrumentation

Request

  1. Confirm: is this a regression in 2.1.119+ vs intentional behavior change?
  2. If regression: target fix in upcoming release
  3. If intentional: document in canonical Hooks docs (code.claude.com/docs/en/hooks + code.claude.com/docs/en/plugins-reference) explaining the new behavior + recommended adaptation
  4. Until resolution: acknowledge shutil.copy2 + SHA256 manual fallback as canonical workaround for users hitting the same regression

Additional Context

This issue surfaced during VAN Pipeline workflow operations using Claude Code 2.1.119+. The diagnostic chain spans 42 consecutive batches (v177-v219, April 24 → May 2, 2026). Pattern is reproducible deterministically and persists across:

  • Multiple Claude Code version upgrades (2.1.119 → 2.1.120 → 2.1.121 → 2.1.122 → 2.1.123 → 2.1.126)
  • Multiple session restarts
  • Hook script modifications + re-permission chmod cycles
  • settings.json re-loads via /reload-plugins

Happy to provide additional diagnostic data or run targeted reproduction tests if helpful for triaging.

---

Reported by VAN Pipeline workflow — autonomous agent infrastructure built on top of Claude Code. F197 = internal incident tag for this regression. Audit chain + reproduction steps + workaround code freely re-usable.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗