Hooks Claude Code: Contoh Guardrail yang Selalu Berjalan
Hooks Claude Code dengan contoh repo nyata: event, matcher, JSON di stdin, exit code, output JSON, dan kapan hook harus memblokir atau cukup memberi saran.
Oleh Matthews WongDitinjau pada Dicek dengan Claude Code v2.1.296
Poin utama
Hook di Claude Code adalah perintah, atau handler lain, yang dijalankan Claude Code secara otomatis di titik tertentu dalam loop-nya, misalnya sebelum atau sesudah setiap tool call. Hook menerima event sebagai JSON lewat stdin dan menjawab dengan exit code atau JSON. Berbeda dengan CLAUDE.md yang bisa diikuti atau tidak oleh Claude, hook selalu berjalan setiap kali event-nya cocok.
Setelah pelajaran ini, Anda bisa
- Memilih event dan matcher agar hook berjalan tepat pada saat yang Anda maksud.
- Membaca JSON dari stdin dan menjawab dengan exit code atau output JSON yang tepat.
- Memutuskan apakah sebuah hook harus memblokir aksi atau cukup memberi saran ke Claude.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Hook yang berjalan di sekitar satu tool call
Claude memanggil Edit; PostToolUse mencocokkan Write|Edit dan mengirim JSON ke skrip review, yang menjawab dengan additionalContext dan exit 0; hook PreToolUse yang keluar dengan exit 2 justru akan memblokir tool call.
Ditinjau pada 13 Oktober 2026 · Claude Code v2.1.296
Tombol: ← → langkah · Home / End · K putar atau jeda
Hook adalah perintah yang dijalankan Claude Code di titik-titik tertentu, apa pun keputusan Claude. Dua event mengapit setiap tool call: PreToolUse sebelum tool berjalan, PostToolUse setelah tool berhasil.
Claude memanggil Edit pada sebuah post blog. PreToolUse berjalan lebih dulu, tetapi repo ini tidak mendaftarkan hook di sana, jadi tool call langsung sampai ke tool dan file berubah.
Edit berhasil, jadi PostToolUse berjalan. Matcher-nya, Write|Edit, adalah daftar nama tool yang persis: hanya Write dan Edit yang menjalankan hook ini.
"matcher": "Write|Edit",Claude Code mengirim event ke skrip sebagai JSON lewat stdin. Skrip membaca tool_input.file_path dan hanya memeriksa post yang baru saja disentuh.
file = payload.tool_input?.file_path ?? payload.tool_response?.filePath ?? "";Tebak langkah berikutnya Skrip menemukan masalah gambar. Apa yang terjadi pada edit-nya? (Dibatalkan, lalu Claude mencoba lagi · Tetap berlaku; Claude menerima temuannya · Claude Code bertanya dulu ke Anda)Skrip mencetak satu objek JSON berisi additionalContext lalu keluar dengan exit 0. Claude membaca temuannya di samping hasil tool. Edit tetap berlaku: hook ini memberi saran, tidak pernah memblokir.
hookSpecificOutput.additionalContext … --hook 2>/dev/null || trueExit 2 hanya memblokir sebelum tool berjalan: hook PreToolUse yang keluar dengan exit 2 membatalkan tool call, dan Claude membaca stderr-nya. Setelah tool berjalan, hook hanya bisa melapor.
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2 exit 2Tebak langkah berikutnya Claude menulis ulang file yang sama lewat perintah Bash. Apakah hook-nya berjalan? (Ya, file-nya tetap berubah · Tidak, Bash bukan Write atau Edit)Perintah Bash yang menulis ulang file yang sama tidak pernah memicu hook Edit|Write. Repo ini menggabungkan post lewat skrip, jadi audit terpisah tetap menjadi gerbang yang sebenarnya.
# via Bash: no Edit|Write hook fires node scripts/merge-blog-staging.mjs # the real gate node scripts/audit-blog-figures.mjs
Event apa saja yang bisa didengar hook Claude Code, dan bagaimana matcher memilih?
Hook ditulis di file settings di bawah key hooks, bertingkat tiga: sebuah event seperti PreToolUse, matcher group yang menyaringnya, lalu satu atau lebih handler yang dijalankan. Event punya ritme masing-masing. SessionStart dan SessionEnd berjalan sekali per sesi, UserPromptSubmit dan Stop sekali per giliran, sedangkan PreToolUse dan PostToolUse di setiap tool call: sebelum tool berjalan, dan setelah tool berhasil. Event lain, seperti PreCompact, SubagentStop, dan FileChanged, mencakup sisa siklus hidupnya.
Baca selengkapnya (2)Tutup
Untuk event tool, matcher dicocokkan dengan nama tool. Matcher yang hanya berisi huruf, angka, garis bawah, tanda hubung, spasi, koma, dan | dianggap daftar nama yang persis, jadi Edit|Write hanya cocok dengan dua tool itu. Satu karakter lain saja mengubahnya menjadi regular expression JavaScript tanpa anchor: Edit.* juga cocok dengan NotebookEdit, jadi tulis ^Edit$ kalau yang Anda maksud memang satu tool. Matcher juga peka huruf besar-kecil.
Sebagian besar handler bertipe command, yaitu perintah shell; tipe lainnya http, mcp_tool, prompt, dan agent. Hook command berhenti otomatis setelah 600 detik kecuali Anda mengatur timeout. Hook di .claude/settings.json yang di-commit ikut sampai ke semua orang yang meng-clone repositori, dan hook dari file settings juga berjalan untuk tool call di dalam subagent. Itu sebabnya agent penulis blog di repo ini mendapat pemeriksaan gambar yang sama dengan sesi utama.
Apa yang diterima hook Claude Code, dan bagaimana hook menjawab?
Hook command menerima event sebagai satu objek JSON lewat stdin. Setiap event membawa field seperti session_id, cwd, dan hook_event_name. Event tool menambahkan tool_name dan tool_input, dan PostToolUse juga menerima tool_response, yaitu hasil yang dikembalikan tool. Path yang dikirim tool file di tool_input selalu berupa path absolut.
Baca selengkapnya (2)Tutup
Jawaban pertamanya adalah exit code. Exit 0 berarti sukses, lalu Claude Code membaca JSON apa pun yang dicetak hook. Exit 2 adalah blocking error pada event yang bisa diblokir, dan stderr hook menjadi alasannya. Kode lain, termasuk 1, adalah non-blocking error: aksinya tetap berjalan. Hook kebijakan yang keluar dengan exit 1 tidak menegakkan apa-apa, dan Claude tidak pernah melihat stderr dari hook yang keluar dengan exit 0.
Untuk kendali yang lebih halus, keluar dengan exit 0 dan cetak tepat satu objek JSON. Field khusus event ditaruh di dalam hookSpecificOutput, yang wajib punya hookEventName. Di situ, additionalContext menambahkan catatan yang dibaca Claude di samping hasil tool, dan hook PreToolUse bisa mengembalikan permissionDecision allow, deny, ask, atau defer. Jangan cetak apa pun selain itu ke stdout: satu baris echo dari profil shell sebelum JSON membuat Claude Code membaca seluruh output sebagai teks biasa, tanpa pesan error sama sekali.
Hook Claude Code sebaiknya memblokir, atau cukup memberi saran?
Tergantung kapan event-nya terjadi. Exit 2 di PreToolUse menghentikan tool call sebelum apa pun terjadi. Exit 2 di PostToolUse hanya menampilkan stderr ke Claude, karena tool sudah berjalan; hook PostToolUse tidak bisa membatalkan edit. Hook Stop yang keluar dengan exit 2 membuat Claude terus bekerja, jadi hook itu harus langsung keluar kalau stop_hook_active bernilai true: Claude Code mengabaikan hook Stop yang memblokir delapan kali berturut-turut.
Baca selengkapnya (2)Tutup
Blokir kalau sebuah aksi tidak boleh terjadi sama sekali, misalnya edit ke .env atau perintah yang merusak. Beri saran kalau jawaban yang benar butuh pertimbangan, atau kalau yang diperiksa memang wajar belum konsisten di tengah pekerjaan. Hook gambar di repo ini sengaja hanya memberi saran: gambar sering baru setengah terpasang saat post sedang ditulis, dan komentar di skripnya menyebut gerbang yang memblokir keadaan itu hanya membuat pekerjaan makin sulit tanpa membuat hasilnya lebih baik.
Apa pun pilihannya, hook bersifat deterministik, sesuatu yang tidak dimiliki CLAUDE.md. Dokumentasi menyebut CLAUDE.md sebagai konteks, bukan konfigurasi yang ditegakkan: Claude membacanya dan berusaha mengikutinya. Hook berjalan di setiap event yang cocok, apa pun keputusan Claude. Karena itu dokumentasi menegaskan: kalau sebuah aturan wajib berlaku setiap saat, jadikan hook, bukan instruksi di prompt.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/scripts/review-blog-figures.mjs\" --hook 2>/dev/null || true",
"timeout": 30,
"statusMessage": "Checking whether the figures fit"
}
]
}
]
}
}Seluruh konfigurasi hook bersama di repo ini. Matcher persis Write|Edit menjalankan pemeriksaan gambar setiap kali ada Write atau Edit, dan tidak pernah untuk Bash. Hook ini memakai shell form, bukan exec form, karena butuh 2>/dev/null || true milik shell, dan placeholder path-nya dibungkus tanda kutip ganda seperti yang diminta dokumentasi untuk shell form. timeout: 30 menggantikan default 600 detik; statusMessage adalah teks yang muncul di spinner selama hook berjalan.
if (HOOK_MODE) {
let file = "";
try {
const payload = JSON.parse((await readStdin()) || "{}");
file =
payload.tool_input?.file_path ?? payload.tool_response?.filePath ?? "";
} catch {
process.exit(0); // Not our payload; a hook must never break the edit.
}
const component = WATCHED.component.exec(file);
if (component) only = component[1];
checkPixels = WATCHED.registry.test(file) || WATCHED.inline.test(file);
// Editing anything else — a page, a script, a message file for a post we
// cannot identify — is not a figure change worth a report.
if (!component && !checkPixels && !WATCHED.messages.test(file))
process.exit(0);
}Sisi input. Skrip mem-parse payload dari stdin dan mengambil tool_input.file_path, dengan tool_response.filePath sebagai cadangan. Payload yang gagal di-parse, atau file yang bukan komponen blog, registry gambar, gambar inline, atau file pesan, langsung keluar dengan exit 0. Jadi sebagian besar edit hanya memakan satu kali start Node, tidak lebih.
const shown = ordered.slice(0, HOOK_MAX_FINDINGS);
const omitted = ordered.length - shown.length;
const lines = shown.map(
(f) => `- ${f.component} [${f.check}] ${f.message}`,
);
if (omitted)
lines.push(
`- …and ${omitted} more not listed here (run the script for all)`,
);
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext:
`Figure-fitness review (${scope}) — ADVISORY, nothing is broken:\n${lines.join("\n")}\n` +
`Look at each image before deciding: does its subject match the section's subject? ` +
`If the post is about experience or methodology, draw a diagram (scripts/generate-blog-diagram.mjs) ` +
`instead of borrowing a photograph. Run node scripts/review-blog-figures.mjs for the full report.`,
},
}),
);
}
process.exit(0);Sisi jawaban. Satu objek JSON berisi hookSpecificOutput, hookEventName, dan additionalContext, dan selalu exit 0. Jumlah temuan dibatasi HOOK_MAX_FINDINGS, yaitu 12, supaya pemeriksaan yang berisik tidak membanjiri konteks yang justru ingin dibantunya, dan teksnya dibuka dengan pernyataan jelas bahwa ini hanya saran. Kalau tidak ada temuan, skrip tidak mencetak apa pun.
Kesalahan yang benar-benar membuang waktu
exit 1 dipakai untuk menegakkan aturan
Tanpa JSON yang valid, exit 1 hanyalah non-blocking error dan aksinya tetap berjalan. Pakai exit 2 untuk memblokir. Kalau skrip yang crash atau hilang juga harus memblokir, pasang onFailure: "block" di handler-nya, yang butuh Claude Code v2.1.295 atau lebih baru.
Matcher regex yang terlalu luas
Edit.* adalah regular expression yang dicocokkan di bagian mana pun dari nama tool, jadi juga berjalan untuk NotebookEdit. Pakai daftar persis Edit|Write, atau beri anchor menjadi ^Edit$.
JSON hook yang tidak berpengaruh
Ada dua penyebab yang sama-sama diam. Ada sesuatu yang tercetak sebelum JSON, sering kali echo di profil shell, sehingga output tidak lagi diawali {. Atau field berada di tingkat yang salah, misalnya additionalContext di luar hookSpecificOutput. Jalankan claude --debug lalu cari di log "Hook output does not start with {" atau "Hook JSON output had unrecognized keys".
File yang diubah lewat Bash tidak memicu Edit|Write
Claude Code tidak menjalankan hook PostToolUse dengan matcher Edit|Write kalau file ditulis ulang oleh perintah Bash atau proses di luar Claude Code. Repo ini menggabungkan post baru dengan scripts/merge-blog-staging.mjs lewat Bash, jadi hook tidak pernah melihatnya, dan scripts/audit-blog-figures.mjs tetap menjadi gerbang terpisah. Untuk memantau file apa pun yang menulisnya, tersedia event FileChanged.
Yang akan saya ubah di sini: crash terlihat seperti diam
2>/dev/null || true mengubah crash menjadi jawaban kosong. Claude tidak pernah melihat stderr dari hook yang keluar dengan exit 0, dan karena stderr dibuang, error-nya juga tidak sampai ke debug log. Akibatnya skrip yang rusak terlihat persis seperti "tidak ada temuan". Menghapus 2>/dev/null tetap membuat exit code 0 lewat || true, tetapi error-nya tercatat di debug log.
| Jawaban hook | Di PreToolUse | Di PostToolUse |
|---|---|---|
| exit 0, tanpa output | Alur izin normal berlanjut | Tidak ada yang ditambahkan |
| exit 0 + additionalContext | Alur normal; Claude menerima catatannya | Claude membaca catatan di samping hasil tool |
| exit 2 + stderr | Diblokir; stderr menjadi alasannya | Claude membaca stderr; edit tetap ada |
| exit 1, tanpa JSON | Non-blocking error; tool call tetap berjalan | Non-blocking error; edit tetap ada |
Lakukan di komputer Anda sendiri
Anda butuh Claude Code, jq, dan satu repositori git untuk latihan. Buat satu hook yang memberi saran dan satu gerbang yang memblokir, uji keduanya secara manual, lalu amati di debug log.
Simpan hook pemberi saran sebagai .claude/hooks/todo-note.sh. Hook ini membaca path file yang diedit dari stdin dan, kalau file masih berisi penanda TODO, mencetak satu objek JSON.
#!/bin/bash FILE=$(jq -r '.tool_input.file_path // empty') if [ -n "$FILE" ] && grep -q TODO "$FILE"; then jq -nc '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: "The edited file still contains TODO markers."}}' fi exit 0Simpan gerbang sebagai .claude/hooks/protect-generated.sh. Gerbang ini keluar dengan exit 2 dan alasan di stderr kalau path-nya berada di folder generated.
#!/bin/bash FILE=$(jq -r '.tool_input.file_path // empty') if [[ "$FILE" == */generated/* ]]; then echo "Blocked: files under generated/ are built by a script" >&2 exit 2 fi exit 0Jadikan keduanya executable lalu daftarkan di .claude/settings.json: catatan setelah Edit dan Write, gerbang sebelum keduanya.
chmod +x .claude/hooks/*.sh { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/todo-note.sh" }] } ], "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-generated.sh" }] } ] } }Uji gerbangnya sebelum Claude menjalankannya, dengan mengirim contoh JSON lewat pipe lalu mencetak exit code-nya.
echo '{"tool_input":{"file_path":"src/generated/api.ts"}}' | .claude/hooks/protect-generated.sh echo "exit $?"Jalankan Claude Code dengan debug log, pastikan /hooks menampilkan keduanya, lalu minta Claude mengedit file yang berisi TODO dan file di dalam generated/. Baca apa yang dilakukan setiap hook di log.
claude --debug-file ./hooks.log > /hooks # in another terminal tail -f hooks.log
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Sebuah hook PostToolUse keluar dengan exit 2. Apakah edit-nya dibatalkan?Lihat jawabannya
Jawaban:Tidak. Tool sudah berjalan. Exit 2 di PostToolUse hanya menampilkan stderr hook ke Claude agar Claude bisa menanggapinya. Untuk mencegah edit, blokir di PreToolUse.
2.Hook Anda mencetak JSON yang valid, tetapi tidak terjadi apa-apa dan tidak ada error. Apa yang Anda periksa?Lihat jawabannya
Jawaban:Apakah ada sesuatu yang tercetak ke stdout sebelum JSON, misalnya echo di profil shell, karena output harus diawali {. Lalu apakah additionalContext atau permissionDecision berada di dalam hookSpecificOutput. claude --debug menyebut kedua masalah itu di log.
3.Bagaimana repo ini memastikan hook-nya tidak pernah memblokir edit?Lihat jawabannya
Jawaban:Hook-nya bertipe PostToolUse, yang berjalan setelah tool sehingga tidak bisa menghentikannya. Perintahnya diakhiri 2>/dev/null || true, jadi selalu exit 0, dan skripnya langsung exit 0 untuk payload yang tidak dikenalinya. Timeout 30 detik membatasi berapa lama hook bisa menahan giliran.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.
- Dokumentasi Claude Code: referensi hooks
- Dokumentasi Claude Code: pola matcher
- Dokumentasi Claude Code: output exit code
- Dokumentasi Claude Code: perilaku exit code 2 per event
- Dokumentasi Claude Code: menambah konteks untuk Claude
- Dokumentasi Claude Code: event PostToolUse
- Dokumentasi Claude Code: otomatisasi tindakan dengan hooks
- Dokumentasi Claude Code: JSON hook tidak berpengaruh
- Dokumentasi Claude Code: hook Stop mencapai batas blokir
- Dokumentasi Claude Code: CLAUDE.md adalah konteks, bukan penegakan
- Dokumentasi Claude Code berbahasa Indonesia: referensi hooks