HookStackGitHub
Back to catalogue
WorkflowStopStopWhen the agent finishes its task· non-blocking

Auto-disable Stop hook after N failures

Per-session deduplication mechanism: uses $PPID as a stable session identifier, counts consecutive failures in /tmp, and automatically disables the hook after ≥3 failures without a fix. Shows reactivation instructions. Resets the counter on success. Prevents infinite asyncRewake re-wake loops.

Use cases

  • Prevent a flaky Stop hook from blocking a session indefinitely via asyncRewake
  • Automatically suspend quality checks when errors are known and being fixed
  • Protect any potentially unstable Stop hook without changing its core logic

Providers & tags

Claude Code
#dedup#session#auto-disable#asyncRewake#ppid#resilience

settings.json fragment

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "asyncRewake": true,
            "command": "node .claude/hooks/quality-check.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/session-dedup-autodisable.mjs

#!/usr/bin/env node
// Auto-désactive les hooks Stop qui ont échoué ≥ N fois de suite (Stop)
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';

const MAX_FAILURES = 3;

export function run({
  exists = existsSync,
  readdir = readdirSync,
  readFile = readFileSync,
  counterDir = '/tmp/claude-hook-counters',
} = {}) {
  if (!exists(counterDir)) return null;

  try {
    const counters = readdir(counterDir).filter((f) => f.endsWith('.counter'));
    const toDisable = counters
      .map((f) => {
        try {
          const count = parseInt(readFile(join(counterDir, f), 'utf8').trim(), 10);
          return count >= MAX_FAILURES ? f.replace('.counter', '') : null;
        } catch {
          return null;
        }
      })
      .filter(Boolean);

    if (!toDisable.length) return null;

    const message =
      `[session-dedup] Hooks à désactiver (${MAX_FAILURES}+ échecs) : ${toDisable.join(', ')}\n` +
      '[session-dedup] Supprimez les fichiers /tmp/claude-hook-counters/*.counter pour réactiver.\n';
    return { toDisable, message };
  } catch {
    // Erreur de lecture — ignorer silencieusement
    return null;
  }
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const result = run();
  if (result?.message) process.stderr.write(result.message);
}