claude-code - 💡(How to fix) Fix PostToolUse hooks not firing despite settings.json wiring (Claude Code 2.1.119+, persistent ~42-batch chain) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#55644Fetched 2026-05-03 04:48:06
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Author
Timeline (top)
labeled ×4commented ×1

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.

Root Cause

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.

Fix Action

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).

Code Example

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}')
"

---

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: $?"

---

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

---

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

---

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}")
RAW_BUFFERClick to expand / collapse

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.

extent analysis

TL;DR

The most likely fix for the GLOBAL PostToolUse hook layer regression in Claude Code 2.1.119+ is to apply a manual Python fallback using shutil.copy2 and SHA256 verification until a targeted fix is released.

Guidance

  • Verify that the hook wiring in ~/.claude/settings.json is valid and correctly configured for the PostToolUse hooks.
  • Confirm that the hook scripts are executable and functional in isolation by running them with a simulated PostToolUse payload.
  • Apply the manual Python fallback using shutil.copy2 and SHA256 verification as a temporary workaround for the regression.
  • Monitor the issue and wait for a targeted fix in an upcoming release, which should restore the functionality of the PostToolUse hooks.
  • Review the canonical Hooks docs for any updates on the new behavior and recommended adaptations, in case the change is intentional.

Example

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}")

Notes

The provided workaround has been validated across a 42-batch chain with 100% SHA256 MATCH outcomes, but it introduces a cumulative friction of ~3-

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix PostToolUse hooks not firing despite settings.json wiring (Claude Code 2.1.119+, persistent ~42-batch chain) [1 comments, 2 participants]