Hooks Deterministik vs Agent Judgement di Claude Code

Foto oleh Xunil via Wikimedia Commons (Public domain)
Tanyakan apakah sebuah script yang memegang tool input, isi repository dan sebuah exit code bisa memutuskannya tanpa tahu niat Anda. Kalau bisa — formatting, typecheck, import terlarang, file yang dilindungi — jadikan hook, karena hook jalan pada event-nya entah agen setuju atau tidak. Simpan urusan judgement, misalnya apakah sebuah abstraksi sudah tepat, di lapisan prosa yang masih bisa diajak berunding.
Bisa, pada event yang mendukung pemblokiran. Pada PreToolUse, exit code 2 langsung memblokir tool call, dan alasan yang dilihat agen adalah apa pun yang script Anda tulis ke stderr. PostToolUse tidak bisa memblokir karena tool-nya sudah jalan saat hook itu menyala; ia hanya bisa mengirim pesan balik agar agen memperbaiki apa yang baru saja ia tulis.
Kemungkinan besar ia ditempelkan ke PostToolUse, yang jalan sekali per tool sehingga jalan bersamaan ketika agen melakukan tool call paralel. Typecheck yang terukur sekitar 3,5 detik di repository saya lalu jalan beberapa kali untuk satu batch write. Memindahkan script yang sama ke PostToolBatch, yang jalan tepat sekali setelah seluruh batch selesai, menghilangkan duplikasi itu tanpa kehilangan umpan baliknya.
Pesan itu menyebut aturannya, file yang bermasalah, dan jalur yang memang berhasil, dicetak ke stderr, karena pada PreToolUse teks itulah satu-satunya alasan yang diterima agen. Exit 2 telanjang memblokir write tanpa mengajarkan apa pun, sehingga agen mencoba lagi hal yang hampir sama. Pesan yang menyebut file dan penggantinya mengubah blokir menjadi satu edit tanpa langkah pencarian.
Ada. Handler hook dengan type prompt mengirim input hook ke sebuah model, Haiku secara default, lalu membaca verdict berupa ok true atau ok false beserta reason, sehingga pemicunya tetap deterministik sementara verdict-nya dinilai. Agent hook melangkah lebih jauh dan bisa membaca file serta menjalankan command sebelum memutuskan, tetapi dokumentasinya menandainya eksperimental dan menyarankan command hook untuk workflow produksi.

Foto oleh Xunil via Wikimedia Commons (Public domain)
Ringkasan Utama
Hook Claude Code adalah shell command yang ditempelkan pada satu lifecycle event, jadi ia jalan entah agen setuju atau tidak, sedangkan instruction di CLAUDE.md hanyalah saran yang berebut perhatian dan dipatuhi sebagian besar waktu. Pindahkan setiap aturan yang bisa dicek secara mekanis menjadi hook, dan sisakan judgement untuk hal yang tidak bisa diputuskan script mana pun.
Sebuah hook PreToolUse di konfigurasi saya sendiri memblokir tool Write dan Edit pada setiap path berakhiran .tsx sampai satu skill tertentu diaktifkan lewat tool yang tidak dimiliki subagent. Aturannya masuk akal, tetapi syaratnya mustahil dipenuhi, jadi operasi tulis itu tidak pernah mendarat. Jalan yang tersisa adalah membangun file di tempat lain lalu menyalinnya dengan cp, dan itulah yang sekarang saya lakukan. Hook tadi tidak membuat kode lebih aman. Ia hanya memindahkan operasi tulis ke path yang tidak ia awasi.
Itu argumen jujur melawan kebiasaan mengubah setiap aturan menjadi kode, dan saya tetap berpendapat lapisan deterministik seharusnya memikul sebagian besarnya. Tulisan ini adalah garis yang saya tarik di antara keduanya: lima sumbu untuk menilai satu kandidat aturan, apa yang tersisa untuk judgement setelah aturan yang bisa dicek dipindahkan, biaya sisi hook yang jarang disebut penggemarnya, dan satu fitur Claude Code yang membuat verdict hasil penilaian bisa dipicu secara deterministik. Nama event, semantik exit code dan tipe hook diambil dari hooks reference Claude Code; angka waktunya dari repository saya sendiri.
Asimetrinya bukan soal kemampuan. Model jelas mampu menjalankan Prettier, menyadari sebuah import terlarang, atau mengingat bahwa enam file di repository ini digenerate dan bukan diedit tangan. Semua itu ia lakukan dengan benar sebagian besar waktu. Masalahnya ada di distribusi: sebagian besar waktu itu tidak terlihat sampai satu kali ia meleset, dan saat itu perubahannya sudah ter-merge. Dokumentasi Claude Code menyebutnya terang-terangan — hooks memberi kontrol deterministik, sehingga tindakan tertentu selalu terjadi alih-alih bergantung pada model untuk memilih menjalankannya.
Sebuah instruction harus memenangkan perhatian. Ia duduk di context window berdampingan dengan tugas, seluruh percakapan sejauh ini, setiap tool result dan setiap aturan lain di file yang sama, lalu dipatuhi kalau model menimbangnya di atas semua itu. Hook tidak bersaing dengan apa pun. Ia shell command yang ditempelkan pada satu lifecycle event, dijalankan harness ketika event menyala dan matcher cocok, dan exit code-nya dibaca sebagai keputusan — pada PreToolUse, exit 2 langsung memblokir tool call. Pendapat agen tidak pernah masuk hitungan. Itulah perbedaannya, bukan kecanggihan.
Kemampuan adalah sumbu yang keliru, karena kedua lapisan sanggup mengungkapkan hampir semua aturan yang bisa Anda rumuskan. Lima hal berikut yang benar-benar berbeda, dan masing-masing pernah menentukan satu aturan buat saya.
| Sumbu | Hook deterministik | Judgement agen |
|---|---|---|
| Apakah ia selalu jalan | Ya, pada event-nya, entah agen setuju atau tidak | Hanya kalau instruction itu mengalahkan seluruh isi window |
| Bisakah hasilnya dibantah | Tidak. Exit 2 memblokir call dan alasannya adalah apa pun yang script Anda cetak | Bisa. Ia hanya saran, dan argumen tandingan yang masuk akal mengalahkannya |
| Biaya per pemanggilan | Waktu jalan script Anda, sama setiap kali, tanpa token | Token di setiap request yang membawa aturan itu, plus penalaran untuk menerapkannya |
| Apa yang terjadi kalau ia salah | Ia memblokir pekerjaan yang benar dan tidak bisa memberi pengecualian | Ia meloloskan persis hal yang Anda tulis untuk dicegat |
| Aturan seperti apa yang bisa ia ungkapkan | Apa pun yang bisa diputuskan dari path, diff, command string atau exit code | Apa pun, termasuk selera dan niat, tanpa jaminan ia akan jalan |
Baca baris keempat dua kali. Kedua lapisan sama-sama bisa salah, tetapi ke arah berlawanan, dan ongkosnya tidak setara. Instruction yang salah membuat Anda kehilangan satu bug. Hook yang salah membuat Anda kehilangan harness-nya, karena begitu perubahan yang benar diblokir untuk kedua kalinya dengan alasan buruk, seseorang akan mematikan hook itu — dan hooks dimatikan berkelompok, bukan satu per satu.
Pertanyaan pemilahnya bukan seberapa penting aturan itu, melainkan apakah sebuah script yang memegang tool input, isi repository dan sebuah exit code bisa memutuskannya tanpa tahu niat di baliknya. Kalau bisa, aturan itu milik hook betapapun sepele kelihatannya: formatting, typecheck, import terlarang, file yang tidak boleh diedit, bentuk commit message. Rasa penting itulah yang menggoda orang menulis satu paragraf panjang di CLAUDE.md untuk aturan yang bisa dijaga sempurna dan selamanya oleh script enam baris.
// .claude/settings.json — two events, because they answer two questions.
// PreToolUse decides whether the write may happen at all; PostToolBatch
// reports on the batch that just landed and has no matcher of its own.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-generated.sh",
"args": [],
"timeout": 5
}
]
}
],
"PostToolBatch": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/typecheck.sh",
"timeout": 120
}
]
}
]
}
}Dua field membuat sebuah hook tetap sempit, dan keduanya lebih menentukan daripada isi script-nya. Matcher menyaring berdasarkan nama tool — Edit dan Write di atas, sehingga sebuah Bash call tidak pernah memanggil handler-nya. Field if memakai sintaks permission rule, jadi sebuah handler bisa dipersempit sampai ke, misalnya, Bash call yang cocok dengan pola rm saja dan tidak dijalankan di luar itu. Hook yang jalan di setiap tool call hanya untuk menyimpulkan bahwa ia tidak punya pekerjaan adalah alasan paling umum sebuah harness terasa lambat.

Setelah semua aturan yang bisa dicek pindah, isi CLAUDE.md menjadi pendek dan seluruhnya judgement: apakah abstraksi ini tepat, apakah nama ini menjelaskan fungsinya, apakah trade-off ini yang kita mau, apakah orang berikutnya membaca error message ini dan langsung tahu harus berbuat apa. Tidak satu pun punya kondisi lulus yang bisa dihitung script. Menuliskannya sebagai aturan hanya menghasilkan instruction yang terdengar bisa ditegakkan padahal tidak, sambil meninggalkan kesan nyaman bahwa pertanyaannya sudah beres.
Satu hal lain yang hanya dipegang lapisan prosa adalah alasan. Hook melihat sebuah tool call — path, diff, command string. Ia tidak bisa tahu bahwa Anda mengedit file hasil generate karena sedang men-debug generator-nya, atau bahwa commit ini sengaja melewati changelog karena ia sebuah revert. Alasan hidup di lapisan instruction, dan alasan itulah yang membuat pembaca, manusia maupun agen, bisa mengenali pengecualian yang sah, pengecualian yang akan ditolak mentah-mentah oleh hook.
Tulis lapisan instruction sebagai alasan, bukan aturan. Satu paragraf yang menjelaskan mengapa enam file itu digenerate akan bertahan melewati refactor; daftar path-nya basi di hari salah satu file pindah. Dan kalau path itu memang ingin ditegakkan alih-alih dijelaskan, itu memang tidak pernah jadi tugas lapisan prosa.
Ada empat, kira-kira sesuai urutan seberapa banyak waktu saya yang mereka habiskan.
Yang lambat punya solusi nyata, dan solusinya adalah nama event, bukan optimisasi. PostToolUse jalan sekali per tool, artinya ia jalan bersamaan ketika agen melakukan tool call paralel — lima write dalam satu batch berarti lima typecheck berebut CPU yang sama. PostToolBatch jalan tepat sekali, setelah semua call dalam batch selesai dan sebelum request berikutnya ke model, dan ia tidak menerima matcher sama sekali. Memindahkan script yang sama dari event per-tool ke event per-batch mengubah pajak di setiap write menjadi satu kali jalan per batch, dan umpan baliknya tetap sampai sebelum giliran agen berikutnya.
Hook yang tidak bisa dimatikan siapa pun adalah hook yang tidak akan dipertahankan siapa pun. disableAllHooks hanyalah satu boolean di settings file dan ia mematikan semua hook, status line kustom serta command file suggestion sekaligus — tidak ada opt-out per hook di level itu. Guardrail yang menambah beberapa detik di setiap write tidak akan disetel ulang. Ia akan dimatikan, bersama empat hook bagus di sebelahnya.
Hook punya dua tugas dan kebanyakan hanya mengerjakan yang pertama. Memblokir itu mekanismenya; pesannya yang jadi produk. Pada PreToolUse, exit 2 memblokir tool call dan alasan yang diterima agen adalah apa pun yang script tulis ke stderr, jadi exit 2 telanjang memblokir write sambil tidak mengatakan apa-apa — dan langkah agen berikutnya adalah mencoba hal yang kurang lebih sama lagi.
#!/usr/bin/env bash
# PreToolUse on Edit|Write: keep the agent out of the generated files.
# The hook payload arrives on stdin; the path lives at tool_input.file_path.
set -euo pipefail
payload=$(cat)
file=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')
[ -z "$file" ] && exit 0
case "$file" in
*/messages/en.json|*/messages/id.json|*/lib/blog-meta.ts)
# Wrong: blocks, teaches nothing, and gets retried almost verbatim.
# exit 2
# Right: name the rule, the file, and the route that does work. On
# PreToolUse, stderr IS the reason the agent receives, so this text is
# the entire difference between a wall and a guardrail.
cat >&2 <<MSG
Blocked: $file is generated by scripts/merge-blog-staging.mjs.
Edit the staged fragment instead, then re-run the merge:
/tmp/blog-staging/agent-SLUG/en.json
node scripts/merge-blog-staging.mjs
MSG
exit 2
;;
esac
exit 0Bedanya bukan kesopanan, melainkan jumlah retry. Pesan yang menyebut aturannya mencegah write yang sama dicoba dua kali. Pesan yang menyebut file, aturan, dan jalur yang memang berhasil mengubah blokir menjadi satu edit tanpa langkah pencarian di tengahnya, karena agen sudah memegang semua yang ia butuhkan. Alasan yang sama membuat saya membiarkan hook figure-review di repository ini tetap di PostToolUse dan hanya mencetak, bukan memblokir: sebuah component memang sah tidak sinkron dengan figure-nya selama beberapa menit di tengah edit, dan lagi pula PostToolUse tidak bisa membatalkan write — tool-nya sudah terlanjur jalan saat hook melihatnya.

Dikotomi pada judul tulisan ini bukan akhir ceritanya, dan pengecualiannya adalah hal paling berguna yang saya tambahkan ke settings file tahun ini. Handler sebuah hook tidak harus berupa shell command. Dengan type diisi prompt, Claude Code mengirim input hook dan prompt Anda ke sebuah model — Haiku secara default, bisa diganti lewat field model — lalu membaca verdict berupa ok true atau ok false beserta reason. Pemicunya tetap deterministik. Yang dinilai hanya verdict-nya.
// The trigger is still deterministic. Only the verdict is judged.
// Haiku answers by default; the "model" field overrides that.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "A blog component under app/components/blogs was edited this turn. If dateModified in lib/blog-meta.ts was not bumped for that slug, respond with {\"ok\": false, \"reason\": \"bump dateModified for that slug\"}."
}
]
}
]
}
}Ada bentuk yang lebih berat. Agent hook memunculkan subagent yang bisa membaca file dan menjalankan command sebelum memutuskan, dengan timeout default 60 detik dan sampai 50 giliran penggunaan tool; dokumentasinya menandainya eksperimental dan menyarankan command hook untuk workflow produksi. Saya memakai bentuk prompt untuk pertanyaan tentang giliran yang baru saja berlalu, dan command hook untuk segala hal yang bisa diselesaikan script, yang ternyata masih hampir semuanya. Kesimpulan yang saya bawa bukan hook mengalahkan instruction, melainkan bahwa determinisme adalah sifat dari pemicunya, dan pemicu adalah bagian yang tidak boleh Anda serahkan kepada model.
Stack Overflow Developer Survey 2025 mencatat 51 persen developer profesional memakai tool AI setiap hari sementara kepercayaan pada akurasinya jatuh ke titik terendah, dan pasangan angka itu adalah alasan seluruh latihan ini: aturan yang tidak butuh kepercayaan lebih berharga daripada aturan yang butuh. Jadi pilah aturan Anda sekali saja. Apa pun yang bisa diputuskan dari path, diff atau exit code menjadi hook yang pesan gagalnya menyebut perbaikannya; apa pun yang butuh niat tetap jadi prosa, dan tetap cukup pendek untuk dibaca. Lalu ukur berapa ongkos hook paling lambat Anda di setiap write, karena itulah yang sebentar lagi dimatikan.
Sumber & bacaan lanjutan