Bereaksi terhadap peristiwa dengan mod
Tangani peristiwa Claude Code dari mod: amati, tulis ulang, atau jawab panggilan alat, prompt, dan giliran, saring peristiwa mana yang ditangani hook, dan rencanakan untuk mod lain.
Hook adalah penanganan peristiwa: fungsi yang dijalankan Claude Code ketika peristiwa bernama terjadi. Claude Code mengeluarkan peristiwa di setiap titik di mana ia akan bertindak, seperti ketika menjalankan alat, mengirimkan prompt, mengirim permintaan ke model, atau memulai atau mengakhiri sesi. Hook Anda berjalan sebelum Claude Code bertindak, sehingga dapat mengamati peristiwa, menulis ulangnya, atau menjawabnya sebagai pengganti Claude Code. Anda mendaftarkan hook dengan on(eventName, handler).
Bangun mod pertama Anda sebelum Anda mulai di sini. Untuk setiap peristiwa dan bidangnya yang tepat, lihat referensi atau baca tipe untuk build Anda.
Bagaimana hook menangani peristiwa
Hook duduk di antara peristiwa dan apa yang akan dilakukan Claude Code tentangnya, sehingga dapat mengamati peristiwa, menulis ulangnya, atau menjawabnya sendiri. Hook menerima tiga argumen: mods API sebagai $, peristiwa sebagai e, dan penanganan berikutnya sebagai next. Penanganan untuk peristiwa membentuk rantai middleware. next(e) memanggil penanganan berikutnya, yang merupakan hook mod lain atau, di akhir rantai, perilaku Claude Code sendiri, dan diselesaikan dengan hasilnya. Apa yang dilakukan hook Anda dengan next menentukan mana dari ketiga hal itu yang dilakukannya.
Amati peristiwa
Untuk mengamati peristiwa tanpa mengubahnya, lakukan pekerjaan Anda dan kembalikan next(e). Hook ini mencatat setiap alat yang akan digunakan Claude:
on('tool.call', async ($, e, next) => {
// Berjalan sebelum alat dijalankan
$.ui.log('Claude is about to use ' + e.tool)
// Teruskan peristiwa tanpa perubahan
return next(e)
})
Sebelum setiap alat dijalankan, baris redup seperti ● my-mod: Claude is about to use Bash muncul dalam transkrip, di mana my-mod adalah nama plugin Anda. Alat berjalan seperti yang akan terjadi tanpa mod.
Untuk bertindak setelah peristiwa, await next(e), lakukan pekerjaan Anda, dan kembalikan hasilnya. Hook ini mencatat setiap alat setelah dijalankan:
on('tool.call', async ($, e, next) => {
// Biarkan alat berjalan, dan tunggu hasilnya
const result = await next(e)
// Berjalan setelah alat selesai
$.ui.log(e.tool + ' finished')
// Berikan hasil kembali tanpa perubahan
return result
})
Baris sekarang muncul setelah setiap alat selesai. Claude membaca hasil yang sama dengan cara apa pun, karena hook mengembalikan apa yang diselesaikan next(e).
Tulis ulang peristiwa
Untuk mengubah apa yang Claude Code bertindak, seperti teks prompt, panggil next dengan salinan peristiwa yang dimodifikasi. Peristiwa itu sendiri tidak dapat diubah: peristiwa itu dibekukan di setiap kedalaman, dan penugasan ke bidang melempar. Hook ini memangkas setiap prompt sebelum dikirim:
on('prompt.submit', async ($, e, next) => {
// Teruskan salinan peristiwa dengan teksnya diubah
return next({ ...e, text: e.text.trim() })
})
Penanganan berikutnya dan Claude Code menerima prompt yang dipangkas dan tidak pernah melihat yang asli. Anda juga dapat mengubah hasilnya: await next(e), kemudian kembalikan salinan hasil dengan bidang yang diganti.
Jawab peristiwa
Untuk menangani peristiwa sendiri, kembalikan hasil tanpa memanggil next. Itu memotong rantai, sehingga mod berikutnya dan perilaku Claude Code sendiri tidak berjalan. Hook ini menolak setiap perintah Bash:
on('tool.call', { tool: 'Bash' }, async () => {
// Tidak ada panggilan ke next, jadi perintah tidak pernah dijalankan
return { deny: 'Bash is turned off in this project. Use the file tools.' }
})
Ketika Claude mencoba perintah Bash, perintah tidak berjalan, dan Claude membaca teks deny sebagai hasil alat. Setiap peristiwa memiliki bentuk hasil sendiri, yang referensi peristiwa daftarkan.
Saring peristiwa mana yang ditangani hook
Untuk menjalankan hook hanya untuk beberapa peristiwa, teruskan filter sebagai argumen kedua ke on. Claude Code memanggil filter sebagai matcher. Ini adalah objek yang bidangnya dibandingkan dengan peristiwa, dan hook hanya berjalan ketika setiap bidang cocok. Bidang dapat berupa nilai, array nilai yang diizinkan, atau ekspresi reguler.
Setiap baris dalam contoh ini mendaftarkan fungsi yang sama, hook, untuk set panggilan alat yang lebih sempit:
// String cocok dengan satu nilai: hanya panggilan Bash
on('tool.call', { tool: 'Bash' }, hook)
// Array cocok dengan nilai apa pun di dalamnya: panggilan Edit dan Write
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// Ekspresi reguler cocok dengan pola: setiap alat dari satu server MCP
on('tool.call', { tool: /^mcp__github__/ }, hook)
hook berjalan sekali untuk panggilan Bash, Edit, atau Write, dan sekali untuk panggilan ke alat yang namanya dimulai dengan mcp__github__. Panggilan ke alat lain apa pun, seperti Read, tidak cocok dengan ketiga hal itu, jadi hook tidak berjalan untuk itu.
Nama peristiwa dapat berupa wildcard. 'classic.*' cocok dengan setiap peristiwa hook pengaturan. '*' cocok dengan setiap peristiwa kecuali peristiwa telemetri, yang Anda hook berdasarkan nama atau sebagai 'telemetry.*'.
Daftarkan setiap peristiwa sekali per matcher. Jika Anda memanggil on dua kali untuk session.start tanpa matcher, modul gagal dimuat dengan on("session.start") is registered twice without a matcher. Masukkan semua yang dilakukan mod Anda saat memulai sesi dalam satu hook.
Hook apa yang dilakukan Claude
Hook peristiwa ini untuk melihat atau mengubah panggilan alat, prompt, atau giliran saat terjadi. Untuk setiap peristiwa dan apa yang dapat dikembalikan hook, lihat referensi peristiwa.
Lindungi atau ubah panggilan alat
Hook tool.call melihat setiap alat yang akan digunakan Claude, sehingga dapat menolak panggilan, mengubah argumennya, atau membiarkannya. tool.call diaktifkan ketika Claude Code akan menjalankan alat, termasuk panggilan yang dibuat subagen dan panggilan ke alat MCP. e.tool adalah nama alat dan argumen alat adalah bidang e, seperti e.command untuk Bash. Ketika Anda memanggil next(e), Claude Code menjalankan pemeriksaan izin dan kemudian alat.
Hook ini menolak perintah Bash yang force-push, dan memberi tahu Claude mengapa:
// Matcher membatasi hook ke panggilan Bash, jadi e.command adalah perintah shell
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/git push .*--force/.test(e.command)) {
// Mengembalikan tanpa memanggil next menjawab peristiwa, jadi perintah tidak pernah dijalankan
return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
}
// Setiap perintah lain melanjutkan ke pemeriksaan izin dan kemudian ke Bash
return next(e)
})
Ketika Claude mencoba git push --force, perintah tidak berjalan dan tidak ada prompt izin muncul, karena hook tidak pernah memanggil next. Claude membaca teks deny sebagai hasil alat, jadi tulislah sebagai instruksi yang dapat ditindaklanjuti Claude. Setiap perintah Bash lain berjalan seperti yang akan terjadi tanpa mod.
Untuk bertindak setelah alat telah berjalan, await next(e), lakukan pekerjaan Anda, dan kembalikan apa yang diberikan next. Hook ini mencatat setiap file .mdx yang diubah Claude, dengan $.ui.log, yang menambahkan baris redup ke transkrip yang tidak dibaca Claude:
on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
// Tunggu pemeriksaan izin dan alat, dan simpan apa yang mereka hasilkan
const result = await next(e)
// Panggilan yang ditolak kembali sebagai { deny }, dan yang gagal memiliki isError yang ditetapkan
const changed = !result.deny && !result.isError
if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
// Kembalikan hasil seperti yang datang, sehingga Claude membaca apa yang dikembalikan alat
return result
})
Setelah Claude mengedit atau menulis file .mdx, baris redup dalam transkrip menamai file. Tidak ada yang dicatat untuk jenis file lain, atau untuk panggilan yang ditolak atau gagal. Pandangan Claude tentang panggilan tidak berubah, karena hook mengembalikan hasil yang diterima.
Untuk mengubah panggilan, teruskan argumen yang diubah ke next. Untuk mencoba ulang panggilan, panggil next(e) lagi: hook yang melihat isError pada hasil pertama dapat menjalankan alat kedua kali dan mengembalikan hasil itu. Untuk menjawab panggilan sendiri, kembalikan objek dengan bidang result, seperti { result: 'Skipped by my-mod' }, tanpa memanggil next. Ketika Anda melakukan itu, tidak ada prompt izin yang muncul dan alat tidak berjalan, jadi hasil yang Anda kembalikan adalah semua yang dipelajari Claude tentang apa yang terjadi.
Hook dalam pengaturan terkelola organisasi Anda berjalan sebelum hook tool.call mod apa pun, dan blok dari salah satu dari mereka adalah final.
Tahan panggilan alat sampai pengguna memutuskan
Hook dapat menjeda panggilan alat dan menanyakan kepada pengguna apa yang harus dilakukan sebelum melanjutkan. Hook tool.call dapat await sebelum memanggil next atau mengembalikan, dan panggilan alat tetap tertunda sampai saat itu. Untuk mengajukan pertanyaan kepada pengguna, panggil $.ui.ask. Ini menampilkan pertanyaan Anda di atas daftar bernomor opsi Anda, dalam dialog yang digunakan Claude untuk menanyakan sesuatu kepada Anda, dan diselesaikan dengan label yang dipilih pengguna. Setelah opsi Anda, dialog menambahkan baris untuk mengetik jawaban berbeda dan baris Chat about this.
Pola RISKY dalam contoh ini cocok dengan rm -r, rm -rf, git reset --hard, dan git push dengan --force, dan melewatkan ejaan lain seperti git push -f. Modul ini menanyakan sebelum menjalankan perintah Bash yang cocok dengan pola:
const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/
export function register(on) {
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
// Biarkan setiap perintah lain melalui tanpa pertanyaan
if (!RISKY.test(e.command)) return next(e)
// Mulai dari jawaban yang aman, sehingga pertanyaan yang tidak dijawab siapa pun menolak perintah
let answer = 'Refuse'
try {
// Panggilan alat menunggu di sini sampai pengguna memilih salah satu dari dua label
answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
} catch {
// Pengguna membatalkan pertanyaan, atau ini adalah jalankan claude -p tanpa siapa pun untuk ditanyai
}
if (answer !== 'Run it') {
// Jawab tanpa memanggil next, jadi perintah tidak berjalan
return { deny: 'The user declined this command. Ask before trying a different approach.' }
}
return next(e)
})
}
Ketika Claude mencoba perintah seperti rm -rf build, pertanyaan muncul dengan perintah di dalamnya, dan perintah menunggu jawaban:
- Pengguna memilih Run it: hook memanggil
next(e), dan pemeriksaan izin biasa masih berjalan setelahnya - Pengguna memilih Refuse: perintah tidak berjalan, dan Claude membaca teks
deny - Pengguna mengetik jawaban:
$.ui.askdiselesaikan dengan teks yang diketik. Hook membandingkannya denganRun it, jadi teks lain apa pun menolak perintah. - Tidak ada yang menjawab:
$.ui.askmenolak ketika pengguna membatalkan pertanyaan atau memilih Chat about this, dan dalam jalankanclaude -p, jadi blokcatchmembiarkan jawaban diRefuse
Simpan tunggu di dalam panggilan mods API seperti $.ui.ask, karena waktu itu tidak dihitung terhadap batas waktu 10 detik hook. Waktu yang dihabiskan menunggu janji Anda sendiri dihitung. Claude Code melewati hook yang habis waktu, jadi perintah yang ditahan akan berjalan.
Tulis ulang atau tambahkan ke prompt
Hook prompt.submit melihat setiap prompt sebelum giliran dimulai, sehingga dapat menulis ulang teks atau menambahnya. e.text adalah apa yang diketik.
| Untuk melakukan ini | Kembalikan ini |
|---|---|
| Tulis ulang prompt. Pesan dalam transkrip menampilkan teks baru. | next({ ...e, text: newText }) |
| Tambahkan teks hanya Claude baca, setelah prompt | next({ ...e, context: [...(e.context ?? []), extraText] }) |
| Hentikan prompt dari pengiriman | { drop: 'the reason' } |
Hook ini menambahkan nama cabang saat ini untuk Claude setiap kali prompt menyebutkan permintaan tarik:
on('prompt.submit', async ($, e, next) => {
// Teruskan prompt yang tidak menyebutkan permintaan tarik seperti apa adanya
if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
const git = await $.process.run(['git', 'branch', '--show-current'])
// Di luar repositori git perintah gagal, jadi tidak ada cabang untuk ditambahkan
if (git.exitCode !== 0) return next(e)
// Simpan konteks apa pun yang ditambahkan hook sebelumnya, dan tambahkan satu baris lagi untuk Claude
return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})
Ketika Anda mengirim prompt seperti open a PR for this change, pesan Anda terlihat sama dalam transkrip, dan Claude juga membaca baris seperti Current branch: feature/auth setelahnya. Prompt yang tidak menyebutkan permintaan tarik melewati tanpa perubahan, dan git tidak berjalan.
Peristiwa lain mencakup sisa apa yang dibaca Claude: prompt.section untuk setiap bagian prompt sistem, prompt.context untuk konteks yang dikirim dengan pesan pertama, dan skill.prompt untuk teks skill. Teks dari hook ini yang berubah antara permintaan membatalkan cache prompt.
Ikuti giliran
Giliran adalah semua yang dilakukan Claude sebagai jawaban atas satu prompt. Hook turn.start, turn.step, dan turn.complete untuk mengikuti satu:
| Peristiwa | Kapan diaktifkan | Apa yang dapat dilakukan hook |
|---|---|---|
turn.start |
Giliran dimulai | Amati. e.turnId mengidentifikasi giliran dalam dua peristiwa lainnya. |
turn.step |
Claude Code akan mengirim satu permintaan ke model. Giliran dengan panggilan alat memiliki beberapa. e.agentId ditetapkan untuk permintaan subagen. |
Baca penggunaan token setiap permintaan, kirimkan ke model berbeda dengan next({ ...e, model }), atau jawab tanpa memanggil model |
turn.complete |
Giliran berakhir, termasuk giliran yang diinterupsi pengguna, di mana e.isAborted adalah true. e.answer adalah teks final Claude, e.durationMs berapa lama waktu yang dibutuhkan, dan e.usage total token giliran. Giliran subagen mengeluarkannya dengan e.agentId ditetapkan. |
Amati, atau kembalikan objek dengan bidang text, seperti { text: 'Done in 12 seconds' }, untuk menampilkan baris di bawah jawaban |
Tulis hook turn.step sebagai generator asinkron, karena peristiwa mengalir. yield* next(e) meneruskan respons saat mengalir dan mengevaluasi hasil yang selesai. Hook ini mencatat berapa banyak dari setiap permintaan yang dilayani Claude API dari cache prompt:
// function* membuat hook menjadi generator, yang dapat meneruskan respons sepotong demi sepotong
on('turn.step', async function* ($, e, next) {
// Kirim permintaan, teruskan setiap bagian saat tiba, dan simpan hasil yang selesai
const result = yield* next(e)
// Lewati hasil yang tidak melaporkan hitungan token
if (result.usage) {
$.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
}
// Kembalikan hasil tanpa perubahan, sehingga giliran berlanjut seperti biasa
return result
})
Respons Claude mengalir ke layar seperti yang terjadi tanpa mod. Setelah setiap permintaan selesai, baris redup dalam transkrip memberikan jumlah token yang dibaca dari cache dan jumlah yang ditulis ke dalamnya. Giliran dengan panggilan alat memiliki beberapa permintaan, jadi menambahkan beberapa baris.
result.usage menyimpan empat hitungan token yang dilaporkan Claude API untuk permintaan, ditambah model yang menjawab: input_tokens, output_tokens, cache_read_input_tokens, dan cache_creation_input_tokens. Hook berjalan untuk permintaan subagen juga, jadi periksa e.agentId ketika Anda hanya menginginkan percakapan utama.
Hook peristiwa hook pengaturan
Hook pengaturan adalah hook perintah, HTTP, prompt, dan agen yang Anda konfigurasi dalam file pengaturan. Setiap peristiwa hook pengaturan, seperti Stop, SessionEnd, atau PostToolUse, juga merupakan peristiwa bernama classic. diikuti dengan nama peristiwa hook pengaturan, seperti classic.Stop. e adalah JSON yang diterima hook pengaturan di stdin, termasuk transcript_path.
Hook ini menggunakan Stop, yang diaktifkan ketika Claude selesai merespons, untuk mencatat di mana transkrip sesi disimpan:
on('classic.Stop', async ($, e, next) => {
// e memiliki bidang yang sama yang dibaca hook Stop dalam file pengaturan dari stdin
$.ui.log('Transcript saved at ' + e.transcript_path)
// Teruskan peristiwa, sehingga hook Stop dalam file pengaturan Anda masih berjalan
return next(e)
})
Setiap kali Claude selesai merespons, baris redup dalam transkrip memberikan jalur file transkrip. Hook mengembalikan next(e), sehingga mengamati peristiwa dan tidak mengubah apa pun tentang cara giliran berakhir.
Jalankan bersama mod lain
Beberapa mod dapat hook peristiwa yang sama, dan salah satu dari mereka dapat gagal. Jika mod Anda memblokir panggilan alat, periksa posisinya dalam rantai dan apa yang terjadi ketika hooknya gagal.
Urutan mod berjalan
Hook pada peristiwa yang sama membentuk satu rantai middleware. Setiap next mod memanggil hook mod berikutnya, dan next terakhir mencapai perilaku Claude Code sendiri. Mod pertama adalah terluar: melihat peristiwa sebelum yang lain dan hasil setelah mereka, dan menentukan apakah yang lain berjalan sama sekali. Mod yang lebih baru tidak dapat menghentikan yang lebih awal dari melihat peristiwa.
Claude Code mengurutkan rantai berdasarkan tempat asal setiap mod:
- Guard bawaan
sec-default@builtin, mod yang dibangun ke dalam Claude Code yang/plugindaftarkan sebagaicc-plugin-sec-default, di mana dimuat, mod yang organisasi Anda daftarkan dalamprependPlugins, dan kemudian mod lain apa pun yang dihitung sebagai organisasi Anda dan tidak dalamappendPlugins - Mod yang Anda instal
- Mod yang organisasi Anda daftarkan dalam
appendPlugins - Mod bawaan lain yang dibangun ke dalam Claude Code
Di antara mod yang Anda instal, mod berjalan sebelum mod yang didaftarkan di bawah dependencies dalam manifestnya. Dalam satu modul, hook berjalan dalam urutan register yang disebut on.
Di mana hook pengaturan berjalan dalam urutan
Hook PreToolUse yang dikonfigurasi dalam file pengaturan juga berjalan selama panggilan alat, pada titik tetap dalam rantai mod:
- Hook
PreToolUsedari pengaturan terkelola: berjalan sebelum hooktool.callmod pertama, dan blok dari salah satu dari mereka adalah final, jadi tidak ada mod yang melihat panggilan. - Hook
PreToolUsedari setiap file pengaturan lain dan darihooks/hooks.jsonplugin: berjalan setelah mod terakhir memanggilnext, sebagai bagian dari perilaku Claude Code sendiri. Mod yang menjawabtool.calltanpa memanggilnextmencegah mereka berjalan, dan mod yang memanggilnextmelihat keputusan mereka dalam hasil yang dikembalikan.
tool.check adalah peristiwa di mana Claude Code memutuskan apakah panggilan alat dapat berjalan. Peristiwa ini diaktifkan setelah hook tersebut dan aturan izin telah memutuskan, dan next(e) diselesaikan dengan keputusan mereka. Hook pada tool.check dapat mengembalikan keputusan berbeda, seperti { decision: 'allow' }, sehingga dapat menyetujui panggilan yang diblokir hook di grup kedua. Perluas izin dengan hook mencantumkan keputusan mana yang berlaku atas mod.
Tangani hook yang gagal
Hook yang gagal tidak merusak sesi, dan Anda dapat memutuskan apa yang terjadi sebagai gantinya. Ketika hook tanpa penanganan .catch melempar, habis waktu, atau mengembalikan hasil bentuk yang salah, apa yang terjadi selanjutnya tergantung pada apakah telah memanggil next:
- Gagal sebelum memanggil
next: Claude Code melewatinya, dan penanganan berikutnya berjalan sebagai gantinya - Gagal setelah
nextdiselesaikan: hasil itu berdiri, dan tidak ada yang berjalan kedua kalinya
Satu baris menamai mod, peristiwa, dan alasan, seperti my-mod: tool.call hook skipped: threw Error: boom. Tempat Anda membacanya tergantung pada sesi, seperti Cari tahu mengapa mod tidak melakukan apa pun daftarkan. Hook ui.render yang gambarnya tidak divalidasi dilaporkan berbeda, seperti Bangun pohon dari elemen menjelaskan.
Untuk membuat hook yang memblokir panggilan gagal tertutup, tambahkan penanganan kesalahan .catch yang menjawab sebagai gantinya. Di sini, guard adalah fungsi hook Anda:
// on mengembalikan pendaftaran, dan .catch melampirkan penanganan ke hook itu saja
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
// next.error.kind adalah 'throw' atau 'timeout', yang mengatakan bagaimana guard gagal
return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})
Sementara guard bekerja, penanganan tidak pernah berjalan. Ketika guard melempar atau habis waktu pada panggilan Bash, Claude Code memanggil penanganan dengan peristiwa yang sama. Penanganan mengembalikan { deny }, jadi perintah tidak berjalan, dan Claude membaca teks dengan throw atau timeout di akhir. Tanpa penanganan, Claude Code akan melewati guard dan menjalankan perintah. Penanganan memiliki satu detik untuk menjawab.
Langkah berikutnya
- Gunakan mods API: tambahkan perintah dan alat, panggil model, dan jalankan pekerjaan pada timer
- Gambar di antarmuka: tampilkan apa yang dikumpulkan hook Anda dalam panel atau di atas prompt
- Uji mod: naikkan salah satu peristiwa ini dari tes
- Referensi mods: setiap peristiwa, setiap metode mods API, dan batasnya