Blog

Hooks in Claude Code 2026: stapsgewijze configuratie en 7 kant-en-klare voorbeelden

Markdown

7 kant-en-klare hooks voor .claude/settings.json, klaar om te kopiëren en plakken: bescherming van .env, prettier, force-push naar main, em-dashes en een dev-server. 4 handlertypen en 31 gebeurtenissen.

Lees het volledige artikel

Hooks in Claude Code zijn shellopdrachten, HTTP-aanroepen of korte LLM-controles die automatisch vóór of na een Claude-actie worden uitgevoerd. Ze voegen controles toe aan specifieke toolevents, formatteren code na bewerking en voegen verse context bij sessiestart toe. Een passende PreToolUse-hook kan die aanroep weigeren; deze regexvoorbeelden zijn gemaksguards, geen beveiligingsgrens voor elk bestands- of Git-toegangspad. Hieronder vind je een overzicht van 4 handlertypen, 31 levenscyclusgebeurtenissen, een test om te bepalen “wanneer een regel in een hook thuishoort”, 7 kant-en-klare hooks voor .claude/settings.json en 3 valkuilen die ik zelf ben tegengekomen.

Ailoot-documentatie: API, sleutels en configuratie.

Wat zijn hooks in Claude Code?

Belangrijkste punt

Een hook is een shellopdracht, HTTP-aanroep, korte LLM-prompt of subagent die Claude Code uitvoert als reactie op een specifieke levenscyclusgebeurtenis. In tegenstelling tot instructies in CLAUDE.md, die het model kan negeren of vergeten, wordt een hook elke keer op dezelfde manier uitgevoerd: het is code, geen suggestie.

Een halfjaar lang bewaarde ik de regel “bewerk .env nooit rechtstreeks” in CLAUDE.md. Claude overtrad die ongeveer eens per maand. Niet met opzet; aan het einde van een lange sessie was het die regel gewoon vergeten. Een keer kwam er een extra token in .env terecht; een andere keer overschreef een variabele met een dubbele naam de variabele die in gebruik was.

Een ingestelde hook kan een overeenkomende directe bewerking met een .env-pad weigeren. Bash-redirection, scripts, MCP-tools en andere toegangspaden vereisen aparte controles; dit voorbeeld maakt het bestand niet onveranderlijk.

Hooks zijn shellopdrachten, HTTP-eindpunten of LLM-prompts die je definieert en die automatisch op specifieke momenten in de levenscyclus van Claude Code worden uitgevoerd.

— Anthropic, https://code.claude.com/docs/en/hooks

Met andere woorden: een hook is een opdracht die Claude Code bij een specifieke gebeurtenis uitvoert. Elke bestandsbewerking, Bash-start, sessiestart of voltooide subagent is een gebeurtenis waaraan je een hook kunt koppelen.

Waarom dit belangrijk is voor een vibe coder:

  • Bescherm bestanden met geheimen tegen onbedoelde bewerkingen.
  • Voer na elke codegeneratie automatisch prettier/eslint uit.
  • Blokkeer git push --force naar main zonder expliciete toestemming.
  • Voeg aan het begin van elke sessie verse context toe.
  • Leg de acties van Claude vast voor audits.

CLAUDE.md beheert context. Hooks beheren acties. Dat zijn twee verschillende dingen, en ze door elkaar halen is de belangrijkste beginnersfout.

Waarin verschillen hooks van CLAUDE.md en skills?

Belangrijkste punt

CLAUDE.md is tekst die het model aan het begin van een sessie leest en probeert te volgen. Een hook is een shellopdracht die het Claude Code-proces uitvoert als reactie op een gebeurtenis, onafhankelijk van het model. Een skill is een benoemd pakket instructies dat Claude kan aanroepen met de Skill-tool, zoals een functie. Alle drie zijn bedoeld voor verschillende soorten taken.

De documentatie van Anthropic legt het verschil tussen CLAUDE.md en hooks expliciet uit:

De inhoud van CLAUDE.md wordt na de systeemprompt als een gebruikersbericht aangeleverd, niet als onderdeel van de systeemprompt. Claude leest de inhoud en probeert die te volgen, maar strikte naleving is niet gegarandeerd.

— Anthropic, documentatie over geheugen, https://code.claude.com/docs/en/memory

Anders gezegd: de inhoud van CLAUDE.md bereikt Claude als het eerste bericht van de gebruiker, niet als een systeeminstructie. Het model leest de inhoud en probeert die te volgen. Maar er zijn geen garanties, vooral niet aan het einde van een lange sessie, wanneer de context groeit en oudere instructies worden verdrongen.

Een hook werkt anders. Die staat in settings.json, is niet afhankelijk van het model en wordt door het Claude Code-proces uitgevoerd bij een specifieke gebeurtenis: PreToolUse, PostToolUse, SessionStart en andere. De opdracht wordt uitgevoerd voordat Claude het resultaat ontvangt en kan een actie fysiek blokkeren (exit 2) of een bestand na het bewerken wijzigen (exit 0).

Vergelijking:

FunctieCLAUDE.mdHookSkill
Waar staat het?CLAUDE.md in de hoofdmap van het project.claude/settings.jsonMap .claude/skills/
Wie voert het uit?Het model leest het als onderdeel van een gebruikersberichtHet Claude Code-proces bij een gebeurtenisHet model via de Skill-tool
UitvoeringsgarantieNee (best effort)Een ingestelde passende commandhook voert code uit; fouten en exitgedrag hangen van het event afVia een modelaanroep (met toestemming, maar niet op shellniveau)
Waarvoor is het bedoeld?Projectcontext, toon, stijl, routeringBeperkingen, formattering, controles van specifieke bestandenComplexe procedures in meerdere stappen met parameters
Wanneer wordt het uitgevoerd?Bij elk modelbericht in een sessieBij een levenscyclusgebeurtenisWanneer het model het aanroept
Kan het een actie blokkeren?NeeJa (exit 2)Nee

De volledige CLAUDE.md-template en 6 regels voor een levend bestand staan in de gids CLAUDE.md configureren in 2026: een kant-en-klare template en 6 regels. Lees meer over skills als benoemde mogelijkheden in Claude Code Skills: zo bouw je je eigen bibliotheek.

Een hook is het laagste niveau: geen model, geen interpretatie, alleen shell.

Hooks zijn een van de tools voor context engineering, naast Plan Mode en /compact.

4 hooktypen: Command, HTTP, Prompt, Agent — welke kies je?

Belangrijkste punt

Claude Code heeft 4 handlertypen. Command is een shellscript en het meest voorkomende type. HTTP verstuurt een POST-verzoek naar je URL, voor teams en integraties. Prompt doet een korte Haiku-aanroep voor een semantische controle. Agent maakt een subagent met toegang tot tools voor grondige validatie. Beginners hebben Command en Prompt nodig; daarmee dek je 90% van de taken af.

TypeWat het doetWanneer kies je het?Complexiteit
CommandVoert een shellopdracht uit, ontvangt JSON via stdin en antwoordt met een exitcode en stdoutIn 90% van de gevallen: paden controleren, formatteren, grep op inhoud, lsof, git-bewerkingenlaag
HTTPVerstuurt een POST-verzoek met een JSON-body naar je URL en ontvangt JSON als antwoordAls een regel bij een team/CI hoort: centrale validatie, een gedeeld statusdashboard, auditing op afstandgemiddeld (vereist een server)
PromptStuurt een korte prompt naar Haiku (standaard) en krijgt ja/nee terug voor een semantische beoordelingAls grep niet volstaat: “beoordeel of deze tekst zakelijk klinkt”laag
AgentMaakt een subagent met toegang tot Read/Grep/Glob voor controles in meerdere stappenGrondige validatie: “zoek alle gewijzigde bestanden en controleer of er bijbehorende tests bestaan”hoog

De meest voorkomende fout is iets met Command proberen te doen waarvoor Prompt nodig is. “Gebruik geen em-dashes in .md-bestanden” is bijvoorbeeld een grep-taak, dus Command is ideaal. Maar “schrijf niet in een zakelijke toon” is semantisch en vereist daarom Prompt.

Als je net begint, gebruik dan Command. De 7 kant-en-klare hooks hieronder zijn allemaal Command. HTTP/Prompt/Agent komen later, wanneer je eraan toe bent.

Waar staat settings.json en wat zijn de 4 configuratieniveaus?

Belangrijkste punt

Claude Code zoekt configuratie op 4 plaatsen: enterprise (uitgerold via MDM), gebruiker (~/.claude/settings.json), project (.claude/settings.json in de hoofdmap van de repository) en lokaal (.claude/settings.local.json, niet in git). Bij het begin van een sessie worden alle 4 bestanden samengevoegd, waarbij het lagere niveau voorrang krijgt.

NiveauPadWanneer geldt het?In git?
Enterprise/Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux)Voor het hele team via MDMnee
Gebruiker~/.claude/settings.jsonAlle projecten van één gebruikernee
Project.claude/settings.json in de hoofdmap van de repoDit project, voor het hele teamja
Lokaal.claude/settings.local.jsonAlleen dit project, alleen deze gebruikernee (in .gitignore)

Ik bewaar de 7 hooks die hieronder worden beschreven in het projectbestand .claude/settings.json, zodat ze automatisch worden opgenomen wanneer de repository wordt gekloond en Claude zich in een nieuw venster aan dezelfde regels houdt. Persoonlijke experimentele hooks staan in .claude/settings.local.json, zodat ze het team niet in de weg zitten.

Alle 4 bestanden worden bij het begin van een sessie samengevoegd. Als regels elkaar tegenspreken, krijgt het lagere niveau voorrang (lokaal boven project, project boven gebruiker).

De snelste manier om je eerste hook te maken is het interactieve menu /hooks in Claude Code zelf. Kies een gebeurtenis uit de lijst, selecteer een matcher (bijvoorbeeld Bash of Edit) en plak een opdracht. Claude Code schrijft dan geldige JSON voor je naar settings.json.

31 levenscyclusgebeurtenissen: welke hebben beginners nodig?

Belangrijkste punt

Claude Code heeft momenteel 31 levenscyclusgebeurtenissen, verdeeld over 3 frequenties: eenmaal per sessie (SessionStart/SessionEnd), eenmaal per beurt (UserPromptSubmit/Stop) en bij elke toolaanroep (PreToolUse/PostToolUse). Beginners hebben 4 van de 31 gebeurtenissen nodig: PreToolUse voor beperkingen, PostToolUse voor automatische bewerkingen, SessionStart voor context en Stop voor eindcontroles.

Gebeurtenissen komen op drie frequenties voor: eenmaal per sessie (SessionStart, SessionEnd), eenmaal per beurt (UserPromptSubmit, Stop, StopFailure) en bij elke toolaanroep binnen de agentische lus (PreToolUse, PostToolUse).

— Anthropic, referentie voor hooks, https://code.claude.com/docs/en/hooks

Het is belangrijk om de drie frequenties te begrijpen, zodat je niet in de war raakt:

  1. Eenmaal per sessie - SessionStart, SessionEnd, Setup. Deze worden eenmaal uitgevoerd wanneer Claude Code wordt geopend en gesloten. Gebruik ze om de context te initialiseren en tijdelijke bestanden op te ruimen.
  2. Eenmaal per beurt - UserPromptSubmit, UserPromptExpansion, Stop, StopFailure. Deze worden geactiveerd bij elk bericht van jou en telkens wanneer het model stopt. Gebruik ze om het gesprek vast te leggen en eindcontroles uit te voeren.
  3. Bij elke toolaanroep - PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, PostToolBatch, SubagentStart, SubagentStop. Deze worden geactiveerd bij elke bestandsbewerking, Bash-start, leesactie of zoekactie. Dit is de actiefste laag, waar beperkingen, formattering en validatie thuishoren.

De volledige lijst van 31 gebeurtenissen bevat ook asynchrone/reactieve gebeurtenissen: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. Deze zijn bedoeld voor geavanceerde scenario’s; sla ze voorlopig over.

Test met vijf vragen: wanneer hoort een regel in een hook en wanneer blijft die in CLAUDE.md?

Belangrijkste punt

Als een regel over een actie gaat (niet over context), betrekking heeft op veiligheid/geld/data, met grep/regex kan worden gecontroleerd, moeilijk ongedaan te maken is als die wordt overtreden en Claude die al minstens één keer heeft overtreden, hoort die in een hook. Als minder dan drie van deze vijf antwoorden “ja” zijn, blijft de regel in CLAUDE.md. Ik heb de test op mijn 80 regels toegepast en 5 regels naar hooks verplaatst.

Voor mij werkt dit als een wet, niet als een suggestie. Ik heb de keuze tussen CLAUDE.md en een hook teruggebracht tot een eenvoudige test met vijf vragen. Als minstens drie antwoorden “ja” zijn, gaat de regel in een hook. Minder dan drie betekent dat de regel in de tekst blijft.

De test “wanneer hoort een regel in een hook?”:

  1. Gaat deze regel over een actie in plaats van context? (“voer rm -rf niet uit” is een actie; “ik heb een TypeScript-project” is context.)
  2. Gaat deze regel over veiligheid, geld of data? (“raak .env niet aan”, “publiceer geen sleutels”.)
  3. Kun je dit controleren met grep, regex of de exitcode van een shellopdracht? (“controleer na het bewerken van .md of er geen em-dash in staat.”)
  4. Zouden de gevolgen moeilijk ongedaan te maken zijn als de regel wordt overtreden? (“force-push naar main” is moeilijk ongedaan te maken; “een punt aan het einde vergeten” is eenvoudig.)
  5. Heb je Claude deze regel al minstens één keer zien vergeten?

Voorbeeld: “Gebruik geen em-dashes; gebruik alleen koppeltekens.” Actie? Ja. Veiligheid? Nee. Controleerbaar met grep? Ja. Moeilijk ongedaan te maken? Nee. Overtreden gezien? Ja. 3 van de 5: zet het in een hook.

Tegenvoorbeeld: “Ik heb vibe coders, geen developers; schrijf zonder jargon.” Actie? Meer context. Veiligheid? Nee. Controleerbaar met een regex? Te moeilijk. Moeilijk ongedaan te maken? Nee. Overtreden gezien? Ja. 1 van de 5: houd het in CLAUDE.md.

Vijf van mijn regels slaagden voor de test. Die zijn naar hooks verplaatst. De overige 75% bleef in het tekstbestand staan.

7 hooks die je direct kunt kopiëren

Belangrijkste punt

Hieronder staan 7 kant-en-klare hooks voor .claude/settings.json: .env-bewerkingen blokkeren, prettier automatisch uitvoeren, force-pushes naar main voorkomen, em-dashes controleren, een poort vrijmaken vóór npm run dev, automatisch committen na geslaagde tests en aan het begin van een sessie verse context toevoegen. Kopieer elke hook naar het bestand, start Claude Code opnieuw en het werkt.

Alle 7 hooks staan in één .claude/settings.json-bestand in de hoofdmap van het project. Elke invoer heeft een gebeurtenistype, een toolfilter (matcher) en een shellopdracht. Exitcode 2 weigert een passende PreToolUse-aanroep vóór uitvoering. In PostToolUse is de actie al uitgevoerd: de hook geeft feedback en kan die niet ongedaan maken.

Voor de meeste hookgebeurtenissen blokkeert alleen exitcode 2 een actie. Claude Code behandelt exitcode 1 als een niet-blokkerende fout en gaat door met de actie, hoewel 1 in Unix conventioneel mislukking betekent.

— Anthropic, referentie voor hooks, https://code.claude.com/docs/en/hooks

exit 0 betekent succes. exit 1 betekent een fout, maar Claude gaat door (dit is de eerste antipattern hieronder). exit 2 heeft eventgebonden gedrag: het kan PreToolUse blokkeren, terwijl PostToolUse alleen feedback over een al uitgevoerde actie geeft.

Hook 1. Bewaak directe .env-bewerkingen (PreToolUse, exit 2)

Blokkeert pogingen om bestanden rechtstreeks te bewerken die overeenkomen met .env, .env.local, .env.production enzovoort.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

Wat het doet: vóór elke Edit/Write/MultiEdit haalt de hook het bestandspad op, controleert met grep of het overeenkomt met het .env-patroon en geeft bij een overeenkomst exitcode 2 met een bericht terug. Claude ziet stderr en zegt tegen me: “Ik probeerde .env te bewerken, maar de hook blokkeerde me; vertel me welke variabele ik moet toevoegen.”

Hook 2. Voer prettier automatisch uit na elke codebewerking (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
          }
        ]
      }
    ]
  }
}

Wat het doet: na elke Edit/Write haalt de hook het pad op en voert, als de extensie .ts/.tsx/.js/.jsx/.json is, prettier op de achtergrond uit met &. De hook eindigt altijd met exitcode 0, want dit gaat om formattering en niet om een beperking. Als prettier mislukt, is dat geen ramp; het bestand blijft zoals het was.

Het verschil met hook 1 is fundamenteel: PreToolUse + exit 2 is een harde blokkade vóór de actie. PostToolUse + exit 0 is een zachte bewerking na de actie. Dit zijn twee verschillende werkwijzen voor verschillende taken.

Hook 3. Blokkeer git push --force naar de main-branch (PreToolUse, exit 2)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

Wat het doet: bij elke Bash-start controleert de hook of de opdracht push bevat, --force/--force-with-lease/-f gebruikt en main vermeldt. Als aan alle drie voorwaarden is voldaan, wordt de opdracht geblokkeerd met een duidelijke melding.

Vroeger stond deze regel in hoofdletters in mijn CLAUDE.md. Claude overtrad hem nog steeds ongeveer eens in de twee weken als ik vroeg om “een force-push uit te voeren om mijn slechte commit ongedaan te maken”. Deze hook vangt de getoonde letterlijke opdrachtvormen op, maar aliassen, scripts, een impliciete bestemmingsbranch of een andere Git-client kunnen de tekstcontrole omzeilen. Dwing main-branchbescherming op de remote af en beperk beschikbare agentinloggegevens.

Hook 4. Blokkeer em-dashes in tekstbestanden (PostToolUse, exit 2)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

Wat het doet: PostToolUse met exitcode 2 meldt na de bewerking een mislukte nacontrole aan Claude. Die draait de schrijfactie niet terug en voorkomt haar niet. Als Claude een bestand heeft bewerkt en een em-dash heeft laten staan, mislukt de hook, krijgt Claude feedback en probeert het opnieuw. Na 2–3 sessies stopt Claude helemaal met het invoegen van dit teken.

Let op: grep zoekt specifiek naar het em-dash-teken (U+2014) met $'\xe2\x80\x94', de Bash-notatie om het als UTF-8-bytes te schrijven. Als je eenvoudigweg grep -q '-' schrijft, worden ook gewone koppeltekens gevonden en wordt de controle bij elk bestand uitgevoerd. Eén onzichtbaar teken maakt de hele logica stuk.

Hook 5. Maak poort 3000 vrij vóór npm run dev (PreToolUse, exit 0)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
          }
        ]
      }
    ]
  }
}

Wat het doet: vóór elke npm run dev (of pnpm/yarn) beëindigt de hook het proces op poort 3000. De hook eindigt altijd met exitcode 0, want dit is voorbereiding en geen beperking. Zo voorkom je een hele klasse situaties waarin Claude “poort bezet” ziet, denkt dat er een codeprobleem is en dingen gaat repareren, terwijl de oude server gewoon nog draaide.

Hook 6. Commit automatisch na geslaagde tests (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
          }
        ]
      }
    ]
  }
}

Wat het doet: de hook leest stdin-JSON één keer en accepteert alleen de getoonde exacte testopdrachten. Hij gaat alleen door wanneer tool_response.exit_code expliciet nul is; ontbreekt dat veld in je versie, dan slaat hij committen over in plaats van succes te raden. Vervang src/orders.ts en tests/orders.test.ts door exacte taakbestanden. Al gestagede wijzigingen stoppen het recept zodat ongerelateerd werk niet meegaat. Een mislukte add stopt, een lege diff maakt geen commit en gewone commithooks blijven aan. Test geslaagde, mislukte en ontbrekende-resultaatinvoer offline vóór inschakelen.

Antipatroon: git commit --no-verify aan elke Edit toevoegen. Houd commitcontroles aan. Activeert een controle deze automatisering recursief, beperk dan de trigger of herontwerp de controle; sla verificatie niet standaard over. Bekijk staged diff en taakeigendom vóór automatische commits.

Hook 7. Voeg verse context toe aan het begin van een sessie (SessionStart met additionalContext)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
          }
        ]
      }
    ]
  }
}

Wat het doet: aan het begin van een sessie verzamelt de hook de huidige Git-branch en de lijst met niet-gecommitte wijzigingen. Vervolgens geeft die JSON terug met een veld additionalContext. Claude Code voegt deze tekst toe aan het eerste systeembericht, zodat Claude vanaf het begin weet waar het zich bevindt en wat er nog is overgebleven van de vorige sessie.

Gebruik additionalContext voor informatie over de huidige staat van de omgeving of een zojuist voltooide bewerking die Claude moet kennen: omgevingsstatus (huidige branch, implementatiedoel, actieve feature flags), voorwaardelijke projectregels (welke testopdracht geldt voor het zojuist bewerkte bestand), externe gegevens (openstaande issues die aan jou zijn toegewezen, recente CI-resultaten).

— Anthropic, referentie voor hooks, https://code.claude.com/docs/en/hooks

Uitbreidingen op deze hook: de meest recente commit en de beschrijving ervan ophalen, de status van GitHub Actions, de actieve sprint uit Linear/Jira en openstaande issues voor de huidige repository.

3 valkuilen met hooks waar één op de twee mensen tegenaan loopt

Belangrijkste punt

De eerste valkuil: een hook met exitcode 1 slaagt stilzwijgend en Claude negeert de fout. De tweede: zwaar werk in een hook wordt na een time-out van 60 seconden beëindigd. De derde: een hook ziet alleen de omgevingsvariabelen uit ~/.zshrc, niet die uit de .env van het project. Je kunt elk probleem met één regel oplossen, maar zonder kennis van deze valkuilen lijkt het alsof “hooks niet werken”.

Valkuil 1. Een hook met exitcode 1 slaagt stilzwijgend

Dit is de gevaarlijkste valkuil. Een week lang dacht ik dat mijn .env-bescherming werkte, maar ik bleek in de hook jq -r '.tool_input.file_path' te gebruiken zonder op null te controleren. Wanneer tool_input leeg was, eindigde jq met code 1. Claude behandelde exitcode 1 als “hookfout, doorgaan”, waarna de actie werd uitgevoerd. Ik begreep niet waarom .env nog steeds werd aangeraakt. Pas toen ik de hook handmatig met lege invoer in een terminal uitvoerde, zag ik de oorzaak.

De oplossing is één regel: begin elke hook met een controle op null en eindig met || exit 0. Een onverwachte exit leidt dan tot een “veilige” exit, in plaats van stilzwijgend door te gaan.

# Sjabloon voor een veilige start en afsluiting:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... jouw logica ...
exit 0   # of || exit 0 op één regel

Valkuil 2. Trage hooks worden door een time-out beëindigd

Ik had een hook die na Edit eslint --fix op het hele bestand uitvoerde. Grote bestanden (1000+ regels) deden er 8–10 seconden over. De standaardtime-out voor hooks in Claude Code is 60 seconden, dus ik dacht binnen de limiet te blijven. Maar wanneer er in één minuut meerdere Edits plaatsvinden, stapelen de hooks zich op en komen ze in een wachtrij terecht; sommige worden gewoon overgeslagen. Dat ontdekte ik pas toen in het logboek “hook timed out” stond.

Oplossing: als een hook zwaar werk uitvoert, start dat dan op de achtergrond. Gebruik eslint --fix "$FILE" & zonder op het resultaat te wachten. De hook retourneert dan binnen milliseconden exitcode 0, terwijl het echte werk op de achtergrond wordt uitgevoerd.

# Zware bewerking op de achtergrond:
npx eslint --fix "$FILE" &
exit 0

Valkuil 3. Een hook kan de omgevingsvariabelen van het project niet zien

Ik dacht dat process.env in een hook beschikbaar zou zijn zoals in een Node.js-app. Dat is niet zo. De hook draait in een shell die door het Claude Code-hoofdproces wordt gestart en ziet alleen wat er in ~/.zshrc of ~/.bashrc staat, plus wat Claude Code expliciet doorgeeft. Als je hook afhankelijk is van OPENAI_API_KEY uit .env, moet je .env handmatig inlezen.

Oplossing: lees .env expliciet in de hookopdracht.

# Projectomgeving doorgeven aan de hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Of met dotenv:
dotenv -e .env -- node my-validator.js

Dit is extra lastig in een monorepo, waar verschillende pakketten elk hun eigen .env-bestanden kunnen hebben. Los dit op door PWD door te geven en aan het begin van de opdracht expliciet cd "$PWD" uit te voeren.

Hoe test je hooks voordat je commit?

Belangrijkste punt

Een hook is een shellopdracht. Je kunt die handmatig in een terminal uitvoeren met fictieve stdin, de exitcode en stderr controleren en dat allemaal doen voordat Claude Code de hook uitvoert. Dat bespaart uren aan foutopsporing en beschermt je tegen hooks die hun eigen werking blokkeren.

Basistestscenario. Neem de hookopdracht uit de JSON, open een terminal en voer de JSON in die Claude Code zou versturen:

# Hook 1 testen (.env beschermen):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
  echo "BLOCKED" >&2
  exit 2
} || exit 0
'
echo "Exit code: $?"

Je hoort BLOCKED in stderr te zien en Exit code: 2. Als je exitcode 0 krijgt, werkt je regex niet; pas grep aan.

Te controleren scenario’s:

  1. Succespad - het bestand moet worden geblokkeerd. .env voor hook 1, git push --force main voor hook 3.
  2. Negatief pad - het bestand mag niet worden geblokkeerd. README.md voor hook 1, git push origin feature-branch voor hook 3.
  3. Randgevallen - lege invoer, null-waarden, onverwachte tekens. De hook moet exitcode 0 teruggeven of eindigen met een duidelijke fout, en niet stilzwijgend doorgaan.

Waar experimentele hooks thuishoren. Niet in .claude/settings.json, maar in .claude/settings.local.json (niet in git). Zet daar nieuwe hooks onder PostToolUse, met echo in plaats van de echte actie. Controleer of ze worden uitgevoerd en verplaats ze daarna naar de productieversie van settings.json.

// Experiment in settings.local.json - schrijft naar /tmp/hook-debug.log:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}

Voer 5–10 bewerkingen uit in Claude Code en open /tmp/hook-debug.log om te zien welke bestanden de hook hebben geactiveerd. Als het bestand leeg is, past de matcher niet; pas de regex aan.

Wat nu?

Belangrijkste punt

Begin met PostToolUse-hooks die exitcode 0 teruggeven; die zijn het veiligst en blokkeren niets. Voeg na een week je eerste PreToolUse-hook met exitcode 2 toe om .env te beschermen of force-pushes te voorkomen. Na twee weken is CLAUDE.md geslonken van een contract van 200 regels tot een korte contextbeschrijving van 50–80 regels, met geselecteerde eventcontroles in hooks en harde toegangsgrenzen apart afgedwongen via machtigingen, sandboxing en remotebescherming.

5 stappen voor de komende week:

  1. Doorzoek je CLAUDE.md met grep op woorden als “nooit”, “verboden”, “altijd” en “moet”. Elke dergelijke regel komt in aanmerking voor een hook. Als een regel een opdracht is, dwingt het tekstbestand die niet af.
  2. Begin met PostToolUse + exit 0. Dit zijn de veiligste hooks: ze voeren een wijziging uit, maar blokkeren niets. Prettier (hook 2), de dev-server beëindigen (hook 5), automatisch committen na tests (hook 6).
  3. Voeg pas je eerste PreToolUse + exit 2 toe als je er vertrouwen in hebt. Bescherm bijvoorbeeld .env (hook 1) of voorkom force-pushes (hook 3). Dit is een harde blokkade, en een fout in de grep-opdracht kan jou ook blokkeren.
  4. Test hooks handmatig voordat je commit (zie het gedeelte hierboven). Dat bespaart uren aan foutopsporing.
  5. Gebruik settings.local.json voor experimenten. Zet daar nieuwe hooks met echo in plaats van de echte actie.

Nadat je hooks hebt ingesteld, is de volgende stap in deze reeks wijzigingen in Claude Code ongedaan maken, voor het geval een hook iets gevaarlijks doorlaat. Wil je dat Claude de structuur van je project tussen sessies onthoudt, bouw dan een Second Brain in Claude Code (business/-map + gekoppelde kennisbank). En als je de skills van je project namen wilt geven zodat je ze opnieuw kunt gebruiken, bekijk dan Claude Code Skills.

Heb je toegang tot neurale netwerken via Ailoot nodig? Bekijk dan Ailoot.

Meer details

Vragen en antwoorden

Waarom moet een hook JSON-invoer maar één keer lezen?

Stdin is een stroom: de eerste jq verbruikt die en latere lezingen bereiken EOF. Bewaar de invoer één keer en haal opdracht- en resultaatvelden daaruit. Behandel een afwezig of onbekend resultaat als ongeverifieerd en sla autocommit over.

Vormen de voorbeeldregexhooks een beveiligingsgrens?

Nee. Ze controleren specifieke events en herkenbare opdrachttekst. Shell-redirection, aliassen, scripts, MCP-tools of andere schrijfpaden kunnen ze omzeilen; gebruik beperkte inloggegevens, bestandsrechten, sandboxing en beschermde branches voor harde grenzen.

Alle artikelen