Back to catalogue
WorkflowStopStopWhen the agent finishes its task· non-blocking
Auto-disable Stop hook after N failures
A flaky Stop hook can't trap your session
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.
What does the Auto-disable Stop hook after N failures hook do?
Auto-disable Stop hook after N failures is a Claude Code Stop hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. A flaky Stop hook can't trap your session.
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
Tags
#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)
//
// Contrat partagé : un hook Stop qui veut bénéficier du watchdog incrémente
// /tmp/claude-hook-counters/<slug>.counter à chaque échec (et le supprime en cas
// de succès), puis vérifie l'absence de <slug>.disabled avant de s'exécuter.
// Ce hook pose le marqueur .disabled dès que le compteur atteint MAX_FAILURES.
import { readFileSync, existsSync, readdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
const MAX_FAILURES = 3;
export function run({
exists = existsSync,
readdir = readdirSync,
readFile = readFileSync,
writeFile = writeFileSync,
counterDir = '/tmp/claude-hook-counters',
} = {}) {
if (!exists(counterDir)) return null;
try {
const counters = readdir(counterDir).filter((f) => f.endsWith('.counter'));
const disabled = [];
for (const f of counters) {
let count = 0;
try {
count = parseInt(readFile(join(counterDir, f), 'utf8').trim(), 10) || 0;
} catch {
continue;
}
if (count < MAX_FAILURES) continue;
const slug = f.replace('.counter', '');
const marker = join(counterDir, `${slug}.disabled`);
if (!exists(marker)) writeFile(marker, '');
disabled.push(slug);
}
if (!disabled.length) return null;
const message =
`[session-dedup] Hooks désactivés (${MAX_FAILURES}+ échecs) : ${disabled.join(', ')}\n` +
`[session-dedup] Supprimez ${counterDir}/<slug>.disabled (et .counter) pour réactiver.\n`;
return { disabled, 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);
}