30 hook events gaan af in een Claude Code-sessie. Slechts 3 kunnen terugpraten.

De volledige lijst met hook events van Claude Code, wanneer elk event afgaat, welke 15 kunnen blokkeren, en de stdout-regel die de output van de meeste hooks stilletjes opslokt. Een praktijkreferentie, opgebouwd uit hooks die in productie draaien over duizenden agentsessies.

Twee storingen komen keer op keer terug wanneer mensen hooks aan Claude Code knopen, en ze lijken in niets op elkaar.

De eerste: je voegt een hook toe, er gebeurt niets. Geen fout, geen waarschuwing, geen logregel. De hook draait simpelweg nooit.

De tweede: de hook draait duidelijk wel, je ziet de bijwerkingen op schijf, maar het bericht dat hij voor de agent afdrukt komt nooit aan. De agent gedraagt zich alsof de hook niets heeft gezegd.

Beide komen uit dezelfde hoek. Het hooksysteem is groter en minder uniform dan het handjevol events dat de meeste artikelen behandelt, en de regels over wie tegen de agent mag praten zijn niet de regels die je zou raden. Dit is de referentie die we zelf hadden willen hebben. We bouwen AgentsRoom bovenop deze hooks, en alles hieronder is ofwel geciteerd uit de officiële referentie, ofwel gemeten in productie.

Er zijn 30 events, geen zes

De meeste gidsen behandelen PreToolUse, PostToolUse, UserPromptSubmit, Stop, Notification en SubagentStop. Die zes bestaan echt, en ze dragen het meeste nuttige werk. Ze zijn ook een vijfde van wat er is.

De volledige lijst, gegroepeerd naar wat ze waarnemen:

GroepEvents
SessieSessionStart, SessionEnd, Setup
PromptUserPromptSubmit, UserPromptExpansion
ToolsPreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch
PermissiesPermissionRequest, PermissionDenied
BeurtStop, StopFailure
Subagents en takenSubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
ContextPreCompact, PostCompact, InstructionsLoaded
OmgevingFileChanged, CwdChanged, ConfigChange
WorktreesWorktreeCreate, WorktreeRemove
InterfaceNotification, MessageDisplay
MCP-elicitationElicitation, ElicitationResult

Tijdlijndiagram van de 30 hook events van Claude Code in de volgorde waarin ze afgaan tijdens een agentsessie, van SessionStart via UserPromptSubmit, PreToolUse, PostToolUse en Stop tot SessionEnd, met daarbij welke events de agent kunnen blokkeren.

De volgorde waarin events afgaan binnen één sessie. Het toolblok herhaalt zich per tool-aanroep, en het hele promptblok herhaalt zich per beurt.

Een paar hiervan veranderen hoe je over het systeem denkt. PostToolUseFailure bestaat, dus de vraag "heeft de tool gewerkt" is een event en niet iets wat je uit een payload afleidt. PostToolBatch gaat één keer af nadat een batch parallelle tool-aanroepen is afgerond, en dat is de juiste plek om een linter één keer te draaien in plaats van één keer per edit. InstructionsLoaded gaat af wanneer CLAUDE.md wordt gelezen, wat je een aangrijpingspunt geeft om te controleren of de agent echt de regels heeft geladen waarvan jij denkt dat hij ze heeft geladen.

De stdout-regel die de output van de meeste hooks opslokt

Dit is het nuttigste wat op deze pagina staat.

Bij exitcode 0 parseert Claude Code stdout op JSON-uitvoervelden. Maar of die stdout ooit aan de agent wordt getoond, hangt af van het event, en de uitzonderingen vormen een korte lijst. Uit de referentie:

Bij de meeste events wordt stdout naar het debuglog geschreven, maar niet in de transcriptie getoond. De uitzonderingen zijn UserPromptSubmit, UserPromptExpansion en SessionStart, waar stdout wordt toegevoegd als context die Claude kan zien en waarnaar hij kan handelen.

Drie events op dertig. Doe je echo "waarschuwing: deze migratie is destructief" vanuit een PostToolUse-hook en verwacht je dat de agent dat leest, dan gebeurt dat nooit. Je tekst ging naar het debuglog.

Er zijn precies twee manieren om vanuit elk ander event tekst voor de agent te krijgen:

  1. Exit 2 en schrijf naar stderr. Bij exit 2 negeert Claude Code stdout en alle JSON daarin, en voert het stderr terug aan de agent als foutmelding.
  2. Exit 0 en druk een JSON-object af met hookSpecificOutput.additionalContext.

Let op de asymmetrie in de eerste. Exit 0 betekent dat stdout telt en stderr niet. Exit 2 betekent dat stderr telt en stdout volledig wordt weggegooid. Dit omgekeerd hebben is de reden dat een hook er volkomen correct kan uitzien en toch stom blijft.

Elke andere exitcode is een niet-blokkerende fout. De transcriptie toont een melding <hook name> hook error met de eerste regel van stderr, de uitvoering gaat door, en de volledige stderr belandt in het debuglog.

Diagram van de exitcodes van Claude Code-hooks: exit 0 stuurt de JSON uit stdout alleen bij drie events naar de agent, exit 2 blokkeert de actie en stuurt stderr naar de agent, elke andere exitcode is een niet-blokkerende fout die naar het debuglog wordt geschreven.

Welk kanaal de agent bereikt, per exitcode. Het gestippelde pad is het pad waarvan mensen aannemen dat het bestaat en dat niet bestaat.

Precies de helft kan blokkeren

Vijftien events stoppen de actie bij exit 2. Vijftien negeren het en gaan door.

Kunnen blokkeren: PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, Elicitation, ElicitationResult, WorktreeCreate.

Kunnen niet blokkeren: PostToolUse, PostToolUseFailure, PermissionDenied, StopFailure, Notification, SubagentStart, SessionStart, Setup, SessionEnd, CwdChanged, FileChanged, PostCompact, WorktreeRemove, InstructionsLoaded, MessageDisplay.

Het praktische gevolg: een vangrail hoort op PreToolUse, nooit op PostToolUse. PostToolUse gaat af nadat de tool is geslaagd. Daar exit 2 doen maakt de schrijfactie niet ongedaan, het drukt alleen een fout af terwijl de schade al op schijf staat. Wil je rm -rf tegenhouden, dan heb je precies één plek om dat te doen.

Dat PostToolBatch wel blokkeert en PostToolUse niet, is een tweede blik waard. Het betekent dat een controle op batchniveau de beurt alsnog kan stoppen nadat parallelle edits zijn geland, en dat is het dichtst bij een veto na het schrijven dat het systeem biedt.

Matchers zijn exact, tot ze het opeens niet meer zijn

Het matcher-veld wisselt van evaluatiestrategie op basis van zijn eigen tekens, en niets vertelt je welke weg het heeft genomen.

MatcherGeëvalueerd als
"*", "", of weggelatenmatcht alles
Alleen letters, cijfers, _, -, spaties, ,, |exacte string, of lijst exacte strings gesplitst op | of ,
Iets andersniet-verankerde JavaScript-reguliere expressie

Niet-verankerd is de valkuil. De referentie is expliciet dat de regex wordt getest met RegExp.prototype.test, wat slaagt bij een match waar dan ook in de waarde. Dus Edit.* matcht Edit en NotebookEdit. Bedoelde je één tool, schrijf dan ^Edit$.

Twee versieafhankelijke gedragingen die je moet kennen voordat je het verkeerde gaat debuggen:

  • Komma's als scheidingsteken en tolerantie voor witruimte vereisen Claude Code v2.1.191 of nieuwer.
  • Koppeltekens kwamen er pas in v2.1.195 bij in de tekenset voor exacte matching. Daarvoor werd een matcher als code-reviewer behandeld als een niet-verankerde regex, en ging hij dus ook af voor senior-code-reviewer.

In AgentsRoom bakenen we onze eigen hook voor bestandstoewijzing af met Write|Edit|MultiEdit|NotebookEdit, wat op het pad van de exacte string blijft en precies die vier tools matcht en verder niets. De lifecycle-hooks die wij installeren hebben helemaal geen matcher, omdat ze ons altijd aangaan.

Zes plekken kunnen hooks definiëren, en ze worden samengevoegd

De reflex is om naar een voorrangsvolgorde te zoeken. Die is er niet, en dat is juist het interessante.

LocatieBereik
~/.claude/settings.jsonal je projecten, lokaal op je machine
.claude/settings.jsonéén project, committeerbaar
.claude/settings.local.jsonéén project, door Claude Code in gitignore gezet
Managed policy settingsde hele organisatie, beheerd door de admin
Plugin hooks/hooks.jsonzolang de plugin aanstaat
Frontmatter van een skill of agentzolang het onderdeel actief is

Uit de referentie:

Hook-entries worden samengevoegd over de settingsniveaus heen in plaats van elkaar te vervangen: gebruikers-, project- en lokale settings voegen hun eigen hooks toe zonder de managed hooks te verwijderen, en de instelling disableAllHooks kan managed hooks niet uitschakelen van buiten de managed settings.

Een projecthook overschrijft dus nooit een globale hook, hij stapelt erbovenop. Zes bronnen, allemaal optellend. Een PostToolUse-formatter die in je gebruikerssettings staat en nog eens in het project, draait twee keer per edit, en het enige symptoom is dat het traag aanvoelt.

Diagram van de zes settingslocaties van Claude Code die hooks kunnen definiëren, die allemaal optellend samenkomen in één set hooks in plaats van elkaar te overschrijven.

Zes bronnen, één samengevoegde set. Niets hier overschrijft iets.

Dit verklaart ook waarom .claude/settings.local.json de juiste plek is voor een tool om een hook in andermans project te installeren. Het is projectgebonden, Claude Code zet het in gitignore, en het wordt geladen zonder enige CLI-vlag. Daar schrijft AgentsRoom zijn entries, zodat de gecommitte .claude/settings.json van een gebruiker nooit wordt aangeraakt en zijn collega's nooit een machinespecifiek pad erven.

Wat hooks draaien in productie ons heeft geleerd

AgentsRoom installeert hooks in elk project dat het opent, om agentstatus deterministisch te volgen en bewerkte bestanden aan de juiste agent toe te schrijven. Sommige dingen komen pas op die schaal boven water.

Onbekende eventnamen worden in stilte genegeerd. Dit staat niet in de documentatie, en wij leunen erop. Wanneer we een nieuw lifecycle-event aan onze installer toevoegen, krijgen gebruikers op een oudere CLI een settings.local.json met een eventnaam waar hun binary nog nooit van heeft gehoord. Er breekt niets, er waarschuwt niets, de entry wordt overgeslagen. Dat maakt het veilig om de installer vóór een CLI-release uit te brengen. Het is ook, onvermijdelijk, de reden dat een typefout volledige stilte oplevert in plaats van een fout.

agent_id is hoe je weet dat je in een subagent zit. Het veld is alleen aanwezig wanneer de hook binnen een subagent-aanroep afgaat. Dat weegt zwaarder dan het klinkt: Stop gaat af wanneer een subagent zijn beurt afmaakt, niet alleen de hoofdagent. Een naïeve regel als "markeer de sessie als klaar bij Stop" markeert de hele sessie als afgerond zodra de eerste de beste subagent terugkomt. Precies daarom slaan wij beurt-events met een agent_id over.

Lees transcript_path niet voor de huidige beurt. De referentie waarschuwt dat de transcriptie asynchroon wordt weggeschreven en kan achterlopen op het gesprek in het geheugen, dus de nieuwste berichten staan er mogelijk nog niet in wanneer jouw hook afgaat. Stop en SubagentStop krijgen last_assistant_message juist zodat je nooit tegen het bestand hoeft te racen.

Hooks zijn het enige betrouwbare statussignaal. Vóór hooks schraapten we de PTY om te bepalen of een agent aan het denken was, wachtte of klaar was. Dat breekt zodra de CLI rendert via de alternate screen buffer van de terminal, wat /tui fullscreen doet. Hooks gaan onder elke renderer identiek af. Bouw je iets dat een agent van buitenaf observeert, dan is dit de laag om op te bouwen, en blijft schrapen hooguit een terugvaloptie.

async: true kost niets. Een hookcommando kan async: true declareren, en dan wacht de agent er niet op. Onze hook doet een POST naar een lokaal endpoint met een limiet van 2 seconden en keert terug; de latency van de beurt van de agent blijft onaangetast, zelfs wanneer de ontvangende app gesloten is. Als je hook alleen observeert en nooit beslist, maak hem dan async en stop met ervoor betalen.

Laat een hook nooit rommel naar de terminal schrijven. Ons script slikt elke exception, ook op het hoogste niveau. Een niet-afgevangen traceback uit een hook faalt niet gewoon stilletjes, hij drukt een Python-stacktrace af in de terminalsessie van de gebruiker, midden in zijn werk.

Timeouts

De standaardwaarden zijn ruim, met drie uitzonderingen die dat niet zijn:

Type hookStandaardtimeout
command, http, mcp_tool600 s
prompt30 s
agent60 s
UserPromptSubmit (command, http, mcp_tool)30 s
MessageDisplay (command, http, mcp_tool)10 s
SessionEnd1,5 s gedeeld over alle hooks, verhoogd om aan te sluiten op een langere timeout per hook, tot maximaal 60 s

Het budget van SessionEnd is degene die mensen verrast. Het is een gedeeld budget, geen toewijzing per hook, dus drie opruimhooks verdelen 1,5 seconde onderling, tenzij je het expliciet verhoogt.

De korte versie

  • Er bestaan 30 events. Zes zijn beroemd.
  • stdout bereikt de agent alleen bij UserPromptSubmit, UserPromptExpansion en SessionStart. Overal elders: exit 2 met stderr, of additionalContext in JSON.
  • 15 events blokkeren bij exit 2, 15 negeren het. Vangrails horen op PreToolUse.
  • Matchers zijn exacte strings, tot een speciaal teken ze in een niet-verankerde regex verandert.
  • Zes settingsbronnen worden optellend samengevoegd. Niets overschrijft iets.
  • Een verkeerd gespelde eventnaam faalt volkomen geruisloos.

Wil je deze events liever zien afgaan dan erover redeneren: dat is wat wij hebben gebouwd. AgentsRoom toont elke hook-trigger per agent, per project, per run, over tientallen parallelle agents en subagent-sessies heen. De hooks die je in je eigen settings configureert blijven precies werken zoals ze geschreven zijn, omdat het de echte CLI draait.

Verder lezen

Download AgentsRoom

Voer je AI-agenten (Claude, Codex, Antigravity CLI, OpenCode, Aider, Grok Build, Mistral Vibe, Kimi Code) uit op al je projecten, vanuit één enkel venster.

GratisDownload AgentsRoom

Companion-app: houd je agents onderweg in de gaten

Breng je eigen: Claude, Codex, Antigravity CLI of andere AI-provider.

Download de extensie
Chrome Web Store

Stuur bugs en verzoeken direct naar je openbare backlog.

Een glimp van AgentsRoom in actie.

Meerdere projecten
Multi-provider
Meerdere agenten
Live status
Bestandsverschil & commit
Mobiele metgezel
Live voorbeeld
Agententeams
Browserautomatisering
Backlog-gedreven ontwikkeling
Promptbibliotheek
Vaardighedenbibliotheek
Bekijk alle functies