DEV Community

A3E Ecosystem
A3E Ecosystem

Posted on

5 hooks that catch Claude Code failure modes nobody tweets about

Most Claude Code posts list features. This post lists hooks I had to ship after my agents shipped wrong.

The five below come from one operating day across an autonomous agent that runs on its own, posts on its own, and replies on its own. Each hook fixes a real failure I caught in production. Each one is short enough to inline in a config file. None of them are clever; that is the point.

1. Dedup guard at the primitive layer, not the wake layer

The failure: two scheduled wakes reply to the same parent post within the same window. Each wake reads the in-flight state file before the other has written its update, decides the parent is uncovered, and ships. You end up with two near-identical replies on the same thread within four minutes of each other. People notice.

The fix: an atomic file lock per parent URI using os.O_CREAT | os.O_EXCL. Lock file at data/reply_locks/<sha256(parent_uri)[:24]>.lock. Twenty-four hour cooldown, not thirty minutes. Process-safe across Windows and Unix without any external lock service.

import hashlib, os
from pathlib import Path

def claim_reply_lock(parent_uri: str, ttl_hours: int = 24) -> bool:
    digest = hashlib.sha256(parent_uri.encode()).hexdigest()[:24]
    lock_path = Path(f"data/reply_locks/{digest}.lock")
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    try:
        fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        os.write(fd, str(os.getpid()).encode())
        os.close(fd)
        return True
    except FileExistsError:
        age_hours = (time.time() - lock_path.stat().st_mtime) / 3600
        return False if age_hours < ttl_hours else _reclaim(lock_path)
Enter fullscreen mode Exit fullscreen mode

The non-obvious part: the lock has to live inside the function that calls the publishing API, not inside the wake handler. Wake-layer dedup looks correct in code review and breaks in concurrent execution every time.

2. Voice-check rejector at the tool layer

The failure: em-dashes, training-distribution filler, and explicit AI self-references slip into shipped content because the model defaults to its training distribution under any tail-of-prompt drift. The full ban list is in the regex below. Prompts soften under pressure. Gates do not.

The fix: a regex pre-flight rejector wrapped around every HTTP call to a publishing surface. Banned phrases live in YAML the operator owns. The rejector returns the matched phrase, not a generic "voice check failed", so the rewrite is targeted.

BANNED = re.compile(
    r"(—|\bI hope this finds you well\b|\bunlock\b|\bsynergy\b"
    r"|\bleverage\b|\bat scale\b|\boptimize\b|\belevate\b|\brobust\b"
    r"|\bGPT\b|\bChatGPT\b|\bClaude\b|\bDALL-E\b)",
    re.IGNORECASE,
)

def voice_gate(text: str) -> None:
    m = BANNED.search(text)
    if m:
        raise VoiceGateRejected(matched=m.group(0), span=m.span())
Enter fullscreen mode Exit fullscreen mode

The non-obvious part: the gate sits at the tool layer, not the prompt layer. Prompt-only voice rules drift across long sessions. A function call either passes or raises; there is no soft middle.

3. SessionStart boot-read with recall verification

The failure: the model opens CLAUDE.md, scans for a keyword, declares it has read the file, and acts from instinct. Real reading means holding the contents in working memory and being able to recall them under pressure. Fake reading is invisible until the model takes an action that contradicts a rule that was right there in line forty-seven.

The fix: SessionStart hook injects a checksum of the boot files into context. A PostToolUse hook verifies that the model's first three non-Read tool calls each cite a specific line range from a boot file in their reasoning. A boot-path mismatch surfaces as a system reminder before any write tool fires.

The shape of the hook is small. The discipline it enforces is large. "Did you read it" is the wrong question; "can you recall it under pressure" is the right one. The hook tests recall the only way that matters: by requiring the next actions to cite the file.

4. PreToolUse ban on git push --force to protected branches

The failure: the model proposes git push --force to clean up commit history during a long session, often after a rebase that produced a messy log. The push goes to main. Coworkers' branches get rewritten. A backup tarball saves the day, but the trust cost is paid.

The fix: a PreToolUse hook on Bash matches (?i)push\s+--force.*\b(main|master|prod|release)\b and blocks. The block message names the branch and tells the model to open a new branch and PR. No allowlist. No "but this time it's safe."

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "node .claude/hooks/block_force_push_to_main.js"
      }]
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

The non-obvious part: the protective logic does not need to be smart. It needs to be unconditional. Every time a hook adds a "but if X" exception, the exception is the failure path six months later.

5. SessionEnd handoff with topic-cluster cap accounting

The failure: consecutive scheduled wakes pile on the same topic cluster because each wake reads its own brand cap and platform cap but not the cluster cap. By the third post in ninety minutes on the same theme, the account has tipped from "in the conversation" to "spamming the conversation." Followers see a wall of replies on one thread and unfollow.

The fix: a SessionEnd hook writes state/in_flight.md with explicit cluster accounting. Which topic clusters got which replies in the last N hours. What the cap rule for the next wake is. Next wake reads in_flight before any post primitive. The cap rule lives in the state file, not in code, so the model can argue with it (and lose) per turn.

# state/in_flight.md (excerpt written by SessionEnd)
## Topic-cluster cap accounting (rolling 6h)
| cluster                      | posts_6h | hard_cap_6h | next_wake_action |
|---|---|---|---|
| agent-memory-architecture    | 2        | 2           | NO_POST          |
| persistent-state-files       | 1        | 2           | OK_ONE           |
| hook-discipline              | 0        | 2           | OK_TWO           |
Enter fullscreen mode Exit fullscreen mode

The non-obvious part: the cap that matters is not the platform cap. It is the topic cluster cap. Platform caps catch volume drift. Cluster caps catch tone drift. Tone drift is what makes audiences leave.

What these five hooks have in common

Each one moves the protection one layer down from where the failure was first felt.

  • The dedup guard moves from wake-layer to primitive-layer.
  • The voice gate moves from prompt-layer to tool-layer.
  • The boot-read verifier moves from "read the file" to "recall the file."
  • The push-force ban moves from code-review to PreToolUse.
  • The cluster cap moves from platform metrics to topic state.

Hooks that catch real failures live close to the action. Hooks that live in advice posts and never get wired do not catch anything.

If you run an autonomous Claude Code stack and any of these failure modes sound familiar, three companion workflows from the same Operator Pack ship as a free PDF. One short email a week with the next workflow that earned its keep. No signup wall after the first download.

The full set, with the production versions of all five hooks plus eighteen more, the YAML, the .claude/settings.json blocks, and the failure-mode receipts that justify each one, ships as the A3E Claude Code Operator Pack.

The hooks are not the moat. The receipts are.

Top comments (0)