PostToolUse hooks not firing despite settings.json wiring (Claude Code 2.1.119+, persistent ~42-batch chain)
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:
- Valid
settings.jsonwiring (verified viapython3 -c "json.load(...)" + grep) - Executable hook script (verified via
ls -la+chmod +x) - Functional smoke test in isolation (hook fires correctly when manually piped simulated
PostToolUseJSON 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.pyinstrumented 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:
- Cumulative friction: ~3-5 min/batch overhead for manual mirror sync × 42 batches = ~120-200 min cumulative
- Observability gap:
instinct-observer.pyPostToolUse hook (used for pattern observation across tool invocations) records zero entries → cognitive companion logging effectively disabled - Hook ecosystem confidence: 8 PostToolUse hooks all silently non-functional → reduces trust in hook layer for new instrumentation
Request
- Confirm: is this a regression in 2.1.119+ vs intentional behavior change?
- If regression: target fix in upcoming release
- 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 - Until resolution: acknowledge
shutil.copy2 + SHA256manual 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.jsonre-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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗