Hooks i Claude Code 2026: trinvis opsætning og 7 eksempler, der er klar til brug
Markdown7 klar-til-brug-hooks til .claude/settings.json, klar til at kopiere og indsætte: beskyttelse af .env, prettier, force-push til main, em-dash og en udviklingsserver. 4 handlertyper og 31 hændelser.
Læs hele artiklen
Hooks i Claude Code er shellkommandoer, HTTP-kald eller korte LLM-tjek, der automatisk kører før eller efter en Claude-handling. De tilføjer kontroller til bestemte værktøjshændelser, formaterer kode efter redigering og indsætter frisk kontekst ved sessionsstart. En matchende PreToolUse-hook kan afvise det pågældende kald; regexeksemplerne er praktiske kontroller, ikke en sikkerhedsgrænse på tværs af enhver fil- eller Git-adgangsvej. Næste del gennemgår 4 handlertyper, 31 livscyklushændelser, en test af “hvornår hører en regel hjemme i en hook?”, 7 hooks, der er klar til at kopiere til .claude/settings.json, og 3 faldgruber, jeg selv er stødt på.
Ailoot-dokumentation: API, nøgler og opsætning.
Hvad er hooks i Claude Code?
Vigtigste pointe
En hook er en shellkommando, et HTTP-kald, en kort LLM-prompt eller en subagent, som Claude Code kører som svar på en bestemt livscyklushændelse. I modsætning til instruktioner i CLAUDE.md, som modellen kan ignorere eller glemme, kører en hook ens hver gang: Det er kode, ikke et forslag.
I et halvt år havde jeg reglen “redigér aldrig .env direkte” i CLAUDE.md. Claude overtrådte den en gang om måneden. Ikke med vilje; den glemte det simpelthen hen mod slutningen af en lang session. En gang endte der et ekstra token i .env; en anden gang overskrev en variabel med et dubletnavn den, der var i brug.
Jeg flyttede reglen til en hook, så den ikke længere kunne glemmes. Når et matchende direkte redigeringsværktøj leverer en .env-sti, afviser den konfigurerede hook dette kald. Bash-omdirigering, scripts, MCP-værktøjer og andre adgangsveje kræver separate kontroller; eksemplet gør ikke filen uforanderlig.
Hooks er shellkommandoer, HTTP-endpoints eller LLM-prompter, som du definerer, og som automatisk kører på bestemte tidspunkter i Claude Codes livscyklus.
Med andre ord er en hook din kommando, som Claude Code kører ved en bestemt hændelse. Hver filredigering, Bash-start, sessionsstart eller afslutning af en subagent er en hændelse, du kan knytte en hook til.
Derfor er det vigtigt for en vibe-koder:
- Beskyt filer med hemmeligheder mod utilsigtede redigeringer.
- Kør automatisk prettier/eslint efter hver kodegenerering.
- Blokér
git push --forcetil main uden udtrykkelig tilladelse. - Indsæt frisk kontekst ved starten af hver session.
- Log Claudes handlinger til revision.
CLAUDE.md håndterer kontekst. Hooks håndterer handlinger. Det er to forskellige ting, og det er den mest almindelige begynderfejl at forveksle dem.
Hvordan adskiller hooks sig fra CLAUDE.md og skills?
Vigtigste pointe
CLAUDE.md er tekst, som modellen læser i starten af en session og forsøger at følge. En hook er en shellkommando, som Claude Code-processen kører som svar på en hændelse, uafhængigt af modellen. En skill er en navngivet pakke med instruktioner, som Claude kan aktivere med Skill-værktøjet, ligesom en funktion. Alle tre løser forskellige slags opgaver.
Anthropics dokumentation forklarer udtrykkeligt forskellen mellem CLAUDE.md og hooks:
Indholdet i CLAUDE.md leveres som en brugermeddelelse efter systemprompten, ikke som en del af systemprompten. Claude læser det og forsøger at følge det, men der er ingen garanti for, at det overholdes strengt.
Sagt på en anden måde: Indholdet i CLAUDE.md når Claude som brugerens første meddelelse, ikke som en systeminstruktion. Modellen læser det og forsøger at følge det. Men der er ingen garanti, især ikke hen mod slutningen af en lang session, hvor konteksten vokser, og ældre instruktioner skubbes ud.
En hook fungerer anderledes. Den ligger i settings.json, afhænger ikke af modellen og køres af Claude Code-processen ved en bestemt hændelse: PreToolUse, PostToolUse, SessionStart og andre. Kommandoen kører, før Claude modtager resultatet, og kan fysisk blokere en handling (exit 2) eller ændre en fil efter redigering (exit 0).
Sammenligning:
| Funktion | CLAUDE.md | Hook | Skill |
|---|---|---|---|
| Hvor den findes | CLAUDE.md i projektets rod | .claude/settings.json | Mappen .claude/skills/ |
| Hvem der kører den | Modellen læser den som en del af en brugermeddelelse | Claude Code-processen ved en hændelse | Modellen via Skill-værktøjet |
| Garanti for udførelse | Nej (best effort) | En konfigureret matchende command-hook udfører kode; fejl og exitadfærd afhænger af hændelsen | Via et modelkald (med en tilladelse, men ikke på shellniveau) |
| Hvad den bruges til | Projektkontekst, tone, stil, routing | Begrænsninger, formatering, tjek af bestemte filer | Komplekse procedurer i flere trin med parametre |
| Hvornår den kører | Ved hver modelmeddelelse i en session | Ved en livscyklushændelse | Når modellen aktiverer den |
| Kan den blokere en handling? | Nej | Ja (exit 2) | Nej |
Den komplette CLAUDE.md-skabelon og 6 regler for en levende fil findes i guiden Sådan konfigurerer du CLAUDE.md i 2026: en skabelon, der er klar til brug, og 6 regler. Læs om skills som navngivne funktioner i Claude Code Skills: Sådan bygger du dit eget bibliotek.
En hook er det laveste niveau: ingen model, ingen fortolkning, kun shell.
Hooks er et af værktøjerne til kontekstteknik sammen med Plan Mode og /compact.
4 hooktyper: Command, HTTP, Prompt, Agent – hvilken skal du vælge?
Vigtigste pointe
Claude Code har 4 handlertyper. Command er et shellscript og den mest almindelige type. HTTP sender en POST-anmodning til din URL til brug for teams og integrationer. Prompt foretager et kort Haiku-kald til et semantisk tjek. Agent opretter en subagent med adgang til værktøjer til dybdegående validering. Begyndere har brug for Command og Prompt; de dækker 90% af opgaverne.
| Type | Hvad den gør | Hvornår du skal vælge den | Kompleksitet |
|---|---|---|---|
| Command | Kører en shellkommando, modtager JSON på stdin og svarer med en exitkode og stdout | 90% af tilfældene: tjek af stier, formatering, grep i indhold, lsof, git-handlinger | lav |
| HTTP | Sender en POST-anmodning til din URL med en JSON-body og modtager JSON som svar | Når en regel hører hjemme i et team/CI: centraliseret validering, et fælles statusdashboard, ekstern revision | middel (kræver en server) |
| Prompt | Sender en kort prompt til Haiku (som standard) og får ja/nej som semantisk vurdering | Når grep ikke er nok: “vurdér, om denne tekst lyder corporate” | lav |
| Agent | Opretter en subagent med adgang til Read/Grep/Glob til tjek i flere trin | Omfattende validering: “find alle ændrede filer, og tjek, at der findes tilsvarende tests” | høj |
Den mest almindelige fejl er at forsøge at bruge Command til noget, der kræver Prompt. For eksempel er “brug ikke em-dash i .md-filer” en grep-opgave, så Command er ideel. Men “skriv ikke i en corporate tone” er semantisk og kræver derfor Prompt.
Hvis du lige er begyndt, så brug Command. De 7 hooks nedenfor, som er klar til brug, er alle Command. HTTP/Prompt/Agent kommer senere, når du er klar.
Hvor ligger settings.json, og hvad er de 4 konfigurationsniveauer?
Vigtigste pointe
Claude Code leder efter konfiguration fire steder: enterprise (udrulles via MDM), bruger (~/.claude/settings.json), projekt (.claude/settings.json i arkivets rod) og lokal (.claude/settings.local.json, ikke i git). Alle 4 filer flettes sammen ved sessionsstart, og det lavere niveau har forrang.
| Niveau | Sti | Hvornår det gælder | I git? |
|---|---|---|---|
| Enterprise | /Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux) | På tværs af teamet via MDM | nej |
| Bruger | ~/.claude/settings.json | Alle projekter for én bruger | nej |
| Projekt | .claude/settings.json i arkivets rod | Dette projekt, for hele teamet | ja |
| Lokal | .claude/settings.local.json | Kun dette projekt, kun denne bruger | nej (i .gitignore) |
Jeg gemmer de 7 hooks, der beskrives nedenfor, i projektets .claude/settings.json, så de automatisk følger med, når arkivet klones, og Claude følger de samme regler i et nyt vindue. Personlige eksperimentelle hooks lægger jeg i .claude/settings.local.json, så de ikke står i vejen for teamet.
Alle 4 filer flettes sammen ved sessionsstart. Hvis regler er i konflikt, har det lavere niveau forrang (lokal over projekt, projekt over bruger).
Den hurtigste måde at oprette din første hook på er den interaktive /hooks-menu direkte i Claude Code. Vælg en hændelse på listen, vælg en matcher (for eksempel Bash eller Edit), og indsæt en kommando. Claude Code skriver gyldig JSON til settings.json for dig.
31 livscyklushændelser: hvilke har begyndere brug for?
Vigtigste pointe
Claude Code har i øjeblikket 31 livscyklushændelser fordelt på 3 frekvenser: én gang pr. session (SessionStart/SessionEnd), én gang pr. tur (UserPromptSubmit/Stop) og ved hvert værktøjskald (PreToolUse/PostToolUse). Begyndere har brug for 4 af de 31 hændelser: PreToolUse til begrænsninger, PostToolUse til automatiske redigeringer, SessionStart til kontekst og Stop til afsluttende tjek.
Hændelser forekommer med tre frekvenser: én gang pr. session (SessionStart, SessionEnd), én gang pr. tur (UserPromptSubmit, Stop, StopFailure) og ved hvert værktøjskald i den agentiske løkke (PreToolUse, PostToolUse).
Det er vigtigt at forstå de tre frekvenser, så du ikke bliver forvirret:
- Én gang pr. session -
SessionStart,SessionEnd,Setup. De kører én gang, når Claude Code åbnes og lukkes. Brug dem til at initialisere kontekst og rydde midlertidige filer. - Én gang pr. tur -
UserPromptSubmit,UserPromptExpansion,Stop,StopFailure. De udløses ved hver af dine meddelelser og hver gang modellen stopper. Brug dem til at logge samtalen og foretage afsluttende tjek af resultatet. - Ved hvert værktøjskald -
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,PostToolBatch,SubagentStart,SubagentStop. De udløses ved hver filredigering, Bash-start, læsning eller søgning. Det er det mest aktive lag, hvor begrænsninger, formatering og validering hører hjemme.
Den komplette liste over 31 hændelser omfatter asynkrone/reaktive hændelser: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. De er til avancerede scenarier; spring dem over foreløbig.
Test med fem spørgsmål: hvornår hører en regel hjemme i en hook, og hvornår skal den blive i CLAUDE.md?
Vigtigste pointe
Hvis en regel handler om en handling (ikke kontekst), vedrører sikkerhed/penge/data, kan tjekkes med grep/regex, er svær at gøre om, hvis den overtrædes, og Claude allerede har overtrådt den mindst én gang, hører den hjemme i en hook. Hvis færre end tre af de fem svar er “ja”, bliver den i CLAUDE.md. Jeg anvendte testen på mine 80 linjer, og 5 regler blev flyttet til hooks.
For mig fungerer det som en lov, ikke et forslag. Jeg kogte valget mellem CLAUDE.md og en hook ned til en enkel test med fem spørgsmål. Hvis mindst tre svar er “ja”, skal reglen i en hook. Hvis færre end tre, bliver den i teksten.
Testen “hvornår hører en regel hjemme i en hook?”:
- Handler reglen om en handling snarere end kontekst? (“kør ikke
rm -rf” er en handling; “jeg har et TypeScript-projekt” er kontekst.) - Handler reglen om sikkerhed, penge eller data? (“rør ikke
.env”, “publicér ikke nøgler”.) - Kan du tjekke den med grep, regex eller en shellkommandos exitkode? (“tjek efter redigering af
.md, at der ikke er noget em-dash.”) - Ville konsekvenserne være smertefulde at gøre om, hvis reglen blev overtrådt? (“force push til main” er svært at gøre om; “glemte et punktum til sidst” er let.)
- Har du allerede set Claude glemme denne regel mindst én gang?
Eksempel: “Brug ikke em-dash; brug kun bindestreger.” Handling? Ja. Sikkerhed? Nej. Kan tjekkes med grep? Ja. Svært at gøre om? Nej. Har du set den overtrådt? Ja. 3 ud af 5: læg den i en hook.
Modeksempel: “Jeg har vibe-kodere, ikke udviklere; skriv uden slang.” Handling? Snarere kontekst. Sikkerhed? Nej. Kan tjekkes med regex? For svært. Svært at gøre om? Nej. Har du set den overtrådt? Ja. 1 ud af 5: behold den i CLAUDE.md.
Fem af mine regler bestod testen. De blev flyttet til hooks. De resterende 75% blev i tekstfilen.
7 hooks, der er klar til at kopiere
Vigtigste pointe
Herunder er 7 hooks, der er klar til brug i .claude/settings.json: blokér redigeringer af .env, kør prettier automatisk, forebyg force-push til main, tjek for em-dash, frigør en port før npm run dev, auto-commit efter vellykkede tests, og indsæt frisk kontekst ved starten af en session. Kopiér hver hook ind i filen, genstart Claude Code, så virker den.
Alle 7 hooks ligger i én .claude/settings.json-fil i projektets rod. Hvert element har en hændelsestype, et værktøjsfilter (matcher) og en shellkommando. Afslutningskode 2 afviser et matchende PreToolUse-kald før udførelse. I PostToolUse er handlingen allerede sket: Hooken giver feedback og kan ikke fortryde den.
For de fleste hookhændelser er det kun exitkode 2, der blokerer en handling. Claude Code behandler exitkode 1 som en fejl, der ikke blokerer, og fortsætter handlingen, selvom 1 konventionelt betyder fejl i Unix.
exit 0 betyder succes. exit 1 betyder en fejl, men Claude fortsætter (det er den første antipattern nedenfor). exit 2 har hændelsesspecifik adfærd: Det kan blokere PreToolUse, mens PostToolUse kun giver feedback om en allerede udført handling.
Hook 1. Kontrollér direkte .env-redigeringer (PreToolUse, exit 2)
Blokerer forsøg på at redigere enhver fil, der matcher .env, .env.local, .env.production og så videre.
{
"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 'Redigér ikke `.env` direkte. Fortæl mig, hvilken variabel jeg skal tilføje, så åbner jeg selv filen.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Hvad den gør: Før hver Edit/Write/MultiEdit henter den filstien, tjekker med grep, om den matcher .env-mønstret, og returnerer i så fald exit 2 med en besked. Claude ser stderr og fortæller mig: “Jeg forsøgte at redigere .env, men hooken blokerede mig; fortæl mig, hvilken variabel jeg skal tilføje.”
Hook 2. Kør prettier automatisk efter hver kodereedigering (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"
}
]
}
]
}
}
Hvad den gør: Efter hver Edit/Write henter den stien og kører prettier i baggrunden med &, hvis filtypen er .ts/.tsx/.js/.jsx/.json. Den afslutter altid med exit 0, fordi det er formatering, ikke en begrænsning. Hvis prettier fejler, gør det ikke noget; filen forbliver, som den var.
Forskellen fra hook 1 er grundlæggende: PreToolUse + exit 2 er en hård blokering før handlingen. PostToolUse + exit 0 er en blød redigering efter handlingen. Det er to forskellige tilstande til forskellige opgaver.
Hook 3. Blokér git push --force til main-branchen (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 til main er forbudt. Opret en ny branch, eller bed mig om at gøre det manuelt.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Hvad den gør: Ved hver Bash-start tjekker den, at kommandoen indeholder push, indeholder --force/--force-with-lease/-f og nævner main. Hvis alle tre betingelser matcher, blokerer den med en tydelig besked.
Jeg plejede at have denne regel med store bogstaver i CLAUDE.md. Claude overtrådte den stadig cirka hver anden uge, når jeg bad den om at “force-pushe for at fortryde min dårlige commit”. Hooken fanger de viste direkte kommandoformer, men aliaser, scripts, en implicit destinationsgren eller en anden Git-klient kan omgå tekstkontrollen. Håndhæv beskyttelse af main på fjernserveren, og begræns agentens tilgængelige legitimationsoplysninger.
Hook 4. Blokér em-dash i tekstfiler (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 \"Filen $FILE indeholder et em-dash (U+2014). Erstat det med en almindelig bindestreg (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Hvad den gør: PostToolUse med exit 2 rapporterer en efterkontrolfejl til Claude efter redigeringen. Det tilbageruller eller forhindrer ikke skrivningen. Hvis Claude har redigeret en fil og efterladt et em-dash, fejler hooken, Claude får feedback og prøver igen. Efter 2–3 sessioner holder Claude helt op med at indsætte dette tegn.
Bemærk, at grep specifikt søger efter em-dash-tegnet (U+2014) ved hjælp af $'\xe2\x80\x94', Bash-notationen for at skrive det som UTF-8-bytes. Hvis du blot skriver grep -q '-', fanger det almindelige bindestreger og kører på hver fil. Et usynligt tegn ødelægger hele logikken.
Hook 5. Frigør port 3000 fø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 frigjort før start' >&2; }; exit 0"
}
]
}
]
}
}
Hvad den gør: Før hver npm run dev (eller pnpm/yarn) afslutter den processen på port 3000. Den afslutter altid med exit 0, fordi det er forberedelse, ikke en begrænsning. Det beskytter mod en type fælde, hvor Claude ser “porten er i brug”, tror, der er et kodeproblem, og begynder at rette ting, selvom den gamle server blot stadig kører.
Hook 6. Opret automatisk en commit efter vellykkede 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'"
}
]
}
]
}
}
Hvad den gør: Den læser JSON-stdin én gang og accepterer kun de viste præcise enkle testkommandoer. Den fortsætter kun, når leveret tool_response.exit_code udtrykkeligt er nul; hvis den installerede version ikke giver feltet, springes commit over frem for at gætte succes. Erstat src/orders.ts og tests/orders.test.ts med præcis de filer, opgaven ejer. Eksisterende staged ændringer stopper opskriften, så uvedkommende arbejde ikke medtages. Fejlslagen add stopper, tom diff giver ingen commit, og almindelige commithooks forbliver aktiveret. Test vellykket, fejlslaget og manglende resultatinput offline før aktivering.
Antipattern: at tilføje git commit --no-verify til hver Edit. Behold commitkontroller aktiveret. Indsnævr udløseren, eller omdesign en kontrol, hvis den starter denne automatisering rekursivt; spring ikke verifikation over som standard. Gennemgå staged diff og opgaveejerskab før automatiske commits.
Hook 7. Indsæt frisk kontekst ved sessionsstart (SessionStart med 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\\\":\\\"Aktuel branch: $BRANCH. Ikke-committede ændringer:\\n$STATUS\\\"}\""
}
]
}
]
}
}
Hvad den gør: Ved sessionsstart henter den den aktuelle Git-branch og listen over ikke-committede ændringer og returnerer derefter JSON med et additionalContext-felt. Claude Code indsætter denne tekst i den første systemmeddelelse, så Claude fra begyndelsen ved, hvor den er, og hvad der er tilbage fra forrige session.
Brug additionalContext til oplysninger, Claude bør kende om miljøets aktuelle tilstand eller en netop afsluttet handling: miljøtilstand (aktuel branch, deploymentmål, aktive feature flags), betingede projektregler (hvilken testkommando der gælder for filen, som netop blev redigeret), eksterne data (åbne issues, der er tildelt dig, seneste CI-resultater).
Udvidelser til denne hook: hent den seneste commit og dens beskrivelse, GitHub Actions-status, den aktive sprint fra Linear/Jira og åbne issues for det aktuelle arkiv.
3 hook-faldgruber, som rammer hver anden person
Vigtigste pointe
Den første faldgrube: En hook med exit 1 passerer lydløst, og Claude ignorerer fejlen. Den anden: Tungt arbejde i en hook afbrydes efter en timeout på 60 sekunder. Den tredje: En hook ser kun miljøvariablerne fra ~/.zshrc, ikke projektets .env. Hver af dem kan løses med én linje, men uden kendskab til faldgruberne kan det virke, som om “hooks ikke virker”.
Faldgrube 1. En hook med exit 1 passerer lydløst
Den farligste. I en uge troede jeg, at min .env-beskyttelse virkede, men det viste sig, at jeg havde jq -r '.tool_input.file_path' i hooken uden at tjekke for null. Når tool_input var tom, afsluttede jq med kode 1. Claude behandlede exit 1 som “hookfejl, fortsæt”, så handlingen blev udført. Jeg kunne ikke forstå, hvorfor .env stadig blev berørt. Jeg så først årsagen, da jeg kørte hooken manuelt i en terminal med tomt input.
Løsningen er én regel: Start hver hook med at tjekke for null, og afslut den med || exit 0. Så medfører en uventet exit en “sikker” afslutning i stedet for at blive ignoreret.
# Skabelon til sikker start og afslutning:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... din logik ...
exit 0 # eller || exit 0 på én linje
Faldgrube 2. Langsomme hooks bliver afbrudt af en timeout
Jeg havde en hook, der kørte eslint --fix på hele filen efter Edit. Store filer (1000+ linjer) tog 8–10 sekunder at behandle. Claude Codes standardtimeout for hooks er 60 sekunder, så jeg troede, at jeg var inden for grænsen. Men når der sker flere redigeringer på ét minut, hober hooks sig op i køen, og nogle springes simpelthen over. Jeg opdagede det først, da loggen viste “hook timed out”.
Løsning: Hvis en hook kræver omfattende arbejde, så kør den i baggrunden. eslint --fix "$FILE" & uden at vente på resultatet. Så returnerer hooken exit 0 på få millisekunder, mens det egentlige arbejde kører i baggrunden.
# Omfattende handling i baggrunden:
npx eslint --fix "$FILE" &
exit 0
Faldgrube 3. En hook kan ikke se projektets miljøvariabler
Jeg troede, at process.env ville være tilgængeligt i en hook ligesom i en Node.js-app. Det er det ikke. Hooken kører i en shell, som startes af den overordnede Claude Code-proces, og ser kun det, der står i ~/.zshrc eller ~/.bashrc, samt det, Claude Code udtrykkeligt sender med. Hvis hooken afhænger af OPENAI_API_KEY fra .env, skal du læse .env manuelt.
Løsning: Læs udtrykkeligt .env i hook-kommandoen.
# Send projektmiljøet videre til hooken:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Eller med dotenv:
dotenv -e .env -- node my-validator.js
Det er især besværligt i et monorepo, hvor forskellige pakker har deres egne .env-filer. Løs det ved at sende PWD videre og udtrykkeligt køre cd "$PWD" i starten af kommandoen.
Hvordan tester du hooks før commit?
Vigtigste pointe
En hook er en shellkommando. Du kan køre den manuelt i en terminal med falsk stdin, tjekke dens exitkode og stderr og gøre det hele, før Claude Code kører den. Det sparer timevis af fejlsøgning og beskytter dig mod hooks, der blokerer deres egen funktion.
Grundlæggende testsituation. Tag hook-kommandoen fra JSON, åbn en terminal, og send den JSON ind, som Claude Code ville sende:
# Test hook 1 (beskyt .env):
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: $?"
Du bør se BLOCKED i stderr og Exit code: 2. Hvis du får exit 0, virker dit regex ikke; ret grep.
Scenarier, der skal tjekkes:
- Positivt scenarie - filen skal blokeres.
.envfor hook 1,git push --force mainfor hook 3. - Negativt scenarie - filen skal ikke blokeres.
README.mdfor hook 1,git push origin feature-branchfor hook 3. - Grænsetilfælde - tomt input, null-værdier, uventede tegn. Hooken skal enten returnere exit 0 eller fejle med en tydelig fejl, ikke passere lydløst.
Hvor eksperimentelle hooks skal placeres. Ikke i .claude/settings.json, men i .claude/settings.local.json (ikke i git). Tilføj nye hooks dér under PostToolUse med echo i stedet for den egentlige handling, tjek om de udløses, og flyt dem derefter til produktionsfilen settings.json.
// Eksperiment i settings.local.json - skriver til /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"
}
]
}
]
}
}
Efter 5–10 redigeringer i Claude Code kan du åbne /tmp/hook-debug.log og se, hvilke filer der udløste hooken. Hvis filen er tom, passer matcheren ikke; ret regexet.
Hvad nu?
Vigtigste pointe
Begynd med PostToolUse-hooks, der afslutter med exit 0; de er sikrest og blokerer ikke noget. Efter en uge kan du tilføje din første PreToolUse-hook med exit 2 for at beskytte .env eller forhindre force-push. Efter to uger er CLAUDE.md skrumpet fra en kontrakt på 200 linjer til en kort kontekstbeskrivelse på 50–80 linjer, mens udvalgte hændelseskontroller er flyttet til hooks, og hårde adgangsgrænser håndhæves særskilt af tilladelser, sandbox og fjernserverbeskyttelse.
5 trin til den kommende uge:
- Grep i din
CLAUDE.mdefter ord som “aldrig”, “forbudt”, “altid” og “skal”. Hver sådan regel kan være en kandidat til en hook. Hvis en regel er et påbud, håndhæver tekstfilen den ikke. - Begynd med PostToolUse + exit 0. De er sikrest: De foretager en ændring, men blokerer ikke noget. Prettier (hook 2), afslutning af udviklingsserveren (hook 5), auto-commit efter tests (hook 6).
- Tilføj først din første PreToolUse + exit 2, når du føler dig sikker. Du kan for eksempel beskytte
.env(hook 1) eller forhindre force-push (hook 3). Det er en hård blokering, og en fejl i grep-kommandoen kan også blokere dig. - Test hooks manuelt før commit (se afsnittet ovenfor). Det sparer timevis af fejlsøgning.
- Brug
settings.local.jsontil eksperimenter. Tilføj nye hooks dér med echo i stedet for den egentlige handling.
Når dine hooks er konfigureret, er næste trin i klyngen sådan fortryder du ændringer i Claude Code, hvis en hook slipper noget farligt igennem. Hvis du vil have Claude til at huske din projektstruktur mellem sessioner, kan du bygge en Second Brain i Claude Code (business/-mappe + sammenkædet vidensbase). Hvis du vil give dit projekts skills navne, så de kan genbruges, kan du se Claude Code Skills.
Kilder
Hvis du har brug for adgang til neurale netværk via Ailoot, så tag et kig på Ailoot.
Flere oplysninger
Spørgsmål og svar
Hvorfor skal en hook læse JSON-input kun én gang?
Stdin er en strøm: Første jq-proces forbruger den, så senere læsninger møder EOF. Gem input én gang, og udtræk derefter kommando- og resultatfelter fra den gemte værdi. Behandl manglende eller ukendt resultat som uverificeret, og spring auto-commit over.
Skaber eksemplernes regex-hooks en sikkerhedsgrænse?
Nej. De kontrollerer bestemte hændelser og genkendelige kommandostrings. Shell-omdirigering, aliaser, scripts, MCP-værktøjer eller en anden skrivevej kan omgå dem. Brug begrænset legitimation, filsystemtilladelser, sandbox og beskyttede grene til hårde grænser.