SpyBara
Go Premium

plugins/mods/events.md 2026-10-27 22:59 UTC to 2026-10-28 23:57 UTC

No changes for this document in this history range.

2026
Thu 1 23:59 Fri 2 22:59 Thu 8 22:58 Fri 9 23:02 Wed 14 22:58 Sat 17 23:57 Tue 20 23:00 Wed 21 23:02 Thu 22 22:58 Fri 23 22:59 Sun 25 23:58 Mon 26 22:57 Tue 27 22:59

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 memerlukan namanya sendiri dan filter { to: 'collector' }.

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

Tangani peristiwa ini untuk melihat atau mengubah panggilan tool, prompt, giliran, atau pembacaan pengaturan saat terjadi. Untuk setiap peristiwa dan apa yang dapat dikembalikan hook, lihat referensi peristiwa.

Lindungi atau ubah panggilan tool

Hook tool.call melihat setiap tool yang akan digunakan Claude, sehingga dapat menolak panggilan, mengubah argumennya, atau membiarkannya lewat. tool.call diaktifkan ketika Claude Code akan menjalankan tool, termasuk panggilan yang dibuat subagent dan panggilan ke tool MCP. e.tool adalah nama tool dan argumen tool adalah bidang e, seperti e.command untuk Bash. Ketika Anda memanggil next(e), Claude Code menjalankan pemeriksaan izin dan kemudian tool tersebut.

Hook ini menolak perintah Bash yang melakukan force-push, dan memberi tahu Claude alasannya:

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

Ketika Claude mencoba git push --force, perintah tidak berjalan dan tidak ada permintaan izin yang muncul, karena hook tidak pernah memanggil next. Claude membaca teks deny sebagai hasil tool, jadi tulislah sebagai instruksi yang dapat ditindaklanjuti Claude. Setiap perintah Bash lain berjalan seperti yang akan terjadi tanpa mod.

Jika hook ini melempar error atau melampaui batas waktu, Claude Code melewatinya dan perintah berlanjut seperti yang akan terjadi tanpa mod. Untuk menolak perintah sebagai gantinya, tambahkan handler .catch ke hook tersebut.

Untuk bertindak setelah tool 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) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

Setelah Claude mengedit atau menulis file .mdx, baris redup dalam transkrip menyebutkan nama file tersebut. 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 diterimanya.

Hook Anda juga dapat mengubah panggilan, mencobanya ulang, menjawabnya sendiri, atau menahan hasilnya:

  • Ubah panggilan: teruskan argumen yang diubah ke next.
  • Coba ulang panggilan: panggil next(e) lagi. Hook yang melihat isError pada hasil pertama dapat menjalankan tool untuk kedua kalinya dan mengembalikan hasil tersebut.
  • Jawab panggilan sendiri: kembalikan objek dengan bidang result, tanpa memanggil next, dan untuk tool bawaan, berikan result bentuk yang dimiliki hasil tool itu sendiri dalam tipe untuk build Anda. Tidak ada permintaan izin yang muncul dan tool tidak berjalan, jadi hasil yang Anda kembalikan adalah satu-satunya yang diketahui Claude tentang apa yang terjadi.
  • Tahan hasil dari Claude: kembalikan { deny: reason } setelah await next(e). Claude membaca alasan Anda sebagai pengganti apa yang dikembalikan next. Ketika tool sudah berjalan, deny menahan hasilnya dari Claude dan tidak membatalkan apa pun yang telah dilakukan tool. Ketika tool berjalan dan berhasil, alasan tersebut mengikuti catatan seperti Bash ran, and a plugin withheld its result:.

Hook dalam pengaturan terkelola organisasi Anda berjalan sebelum hook tool.call mod mana pun. Ketika hook terkelola memblokir panggilan, tidak ada hook tool.call mod yang berjalan untuk panggilan tersebut.

Tahan panggilan tool sampai pengguna memutuskan

Hook dapat menjeda panggilan tool dan menanyakan kepada pengguna apa yang harus dilakukan sebelum melanjutkan. Hook tool.call dapat melakukan await sebelum memanggil next atau mengembalikan nilai, dan panggilan tool 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 penulisan lain seperti git push -f. Modul ini bertanya sebelum menjalankan perintah Bash yang cocok dengan pola tersebut:

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) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      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 tersebut di dalamnya, dan perintah menunggu jawaban:

  • Pengguna memilih Run it: hook memanggil next(e), dan pemeriksaan izin biasa tetap berjalan setelahnya
  • Pengguna memilih Refuse: perintah tidak berjalan, dan Claude membaca teks deny
  • Pengguna mengetik jawaban: $.ui.ask diselesaikan dengan teks yang diketik. Hook membandingkannya dengan Run it, jadi teks lain apa pun menolak perintah.
  • Tidak ada yang menjawab: $.ui.ask menolak ketika pengguna menutup pertanyaan atau memilih Chat about this, dan dalam eksekusi claude -p, sehingga blok catch membiarkan jawaban tetap Refuse

Pertahankan penantian di dalam panggilan API mods seperti $.ui.ask, karena waktu tersebut tidak dihitung terhadap batas waktu hook. Waktu yang dihabiskan untuk menunggu promise Anda sendiri tetap dihitung.

Claude Code melewati hook yang melampaui batas waktu, sehingga perintah yang ditahan akan berjalan. Untuk menolaknya sebagai gantinya, rangkaikan handler .catch ke registrasi yang dikembalikan on. Handler tersebut terpisah dari blok catch di dalam hook.

Setujui atau tolak panggilan tool sebelum pengguna ditanya

Untuk memutuskan apakah panggilan tool boleh berjalan, tangani tool.check, peristiwa tempat Claude Code membuat keputusan tersebut.

Untuk perintah atau path yang tetap, gunakan aturan izin seperti Bash(npm test), yang tidak memerlukan kode. Tangani tool.check ketika keputusan bergantung pada apa yang berlaku pada saat itu, seperti branch Git saat ini atau nilai yang dicatat oleh hook lain.

  • Apa yang dikembalikan: untuk memutuskan panggilan sendiri, kembalikan objek Anda sendiri, seperti { decision: 'allow' } atau { decision: 'deny', reason }. Untuk membiarkan keputusan apa adanya, kembalikan apa yang diberikan await next(e) kepada Anda.
  • Apa yang diberikan await next(e): keputusan Claude Code sendiri, sebagai objek yang decision-nya adalah allow, ask, atau deny. Tidak ada permintaan izin yang muncul selama next(e) berjalan.
  • Argumen: e.input berisi argumen tool, seperti command untuk Bash
  • Kapan tool.check diaktifkan: setelah hook PreToolUse berjalan, dan sebelum permintaan izin, pengklasifikasi auto mode, atau hook PermissionRequest

Hook ini menolak git push saat branch saat ini adalah main:

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // Claude Code's own decision
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

Hook menolak git push di main dan meneruskan setiap panggilan lain:

  • Di main: hook mengembalikan deny, bahkan ketika suatu aturan mengizinkan git push
  • Di branch lain, dan untuk perintah lain: panggilan mendapatkan keputusan yang akan didapatnya tanpa mod
  • Hook melempar error atau melampaui batas waktu: panggilan juga mendapatkan keputusan yang akan didapatnya tanpa mod. Untuk menolak panggilan sebagai gantinya, tambahkan handler .catch yang mengembalikan { decision: 'deny', reason: 'the reason' }.

Hook mencocokkan teks perintah, jadi perlakukan sebagai pengingat untuk Claude. Untuk memblokir push ke main bagi semua orang, lindungi branch tersebut di host Git Anda.

[!NOTE]

Hook tool.check juga dapat mengembalikan allow atau ask. Dalam plan mode, untuk panggilan yang tidak read-only, seperti pengeditan file, hook Anda dapat memperketat keputusan yang dikembalikan next(e) tetapi tidak dapat melonggarkannya. Untuk aturan izin dan keputusan hook PreToolUse yang tidak dapat digantikan oleh allow dari hook Anda, lihat Perluas izin dengan hook.

Periksa apakah panggilan tool bersifat read-only

Dalam hook tool.check, e.isReadOnly bernilai true ketika tool melaporkan panggilan tersebut sebagai read-only. Jika tidak, bidang ini tidak ada, bukan false. e.isReadOnly memerlukan Claude Code v2.1.304 atau yang lebih baru.

Beberapa tool, seperti Read, Grep, Glob, dan WebSearch, melaporkan setiap panggilan sebagai read-only. Bash dan tool MCP melaporkan panggilan sebagai read-only dalam kasus berikut:

Nilai ini adalah laporan tool itu sendiri, jadi jangan menyetujui panggilan hanya berdasarkan bidang ini. Misalnya, panggilan WebFetch dan Agent dilaporkan sebagai read-only, meskipun WebFetch meminta URL dan Agent memulai subagent yang menjalankan tool-nya sendiri.

Hook ini mengembalikan ask untuk setiap panggilan mcp__github__ yang tidak read-only dan belum ditolak:

on('tool.check', { tool: /^mcp__github__/ }, async ($, e, next) => {
  const decided = await next(e)
  // Read-only calls and denied calls pass through unchanged
  if (e.isReadOnly || decided.decision === 'deny') return decided
  return { decision: 'ask', reason: 'This tool can change the GitHub repository' }
})

Filter hook tool.check berdasarkan apakah panggilan bersifat read-only

Untuk membatasi panggilan mana yang ditangani hook Anda berdasarkan e.isReadOnly, gunakan matcher untuk panggilan read-only dan badan hook untuk sisanya:

  • Hanya panggilan read-only: tambahkan isReadOnly: true ke matcher, seperti dalam on('tool.check', { tool: 'Bash', isReadOnly: true }, hook)
  • Panggilan yang tidak read-only: uji e.isReadOnly di badan hook, seperti yang dilakukan contoh mcp__github__. Jika Anda menaruh isReadOnly: false dalam matcher, hook Anda tidak pernah berjalan.

Tulis ulang atau tambahkan ke prompt

Hook prompt.submit melihat setiap prompt sebelum giliran dimulai, sehingga dapat menulis ulang teks atau menambahkan sesuatu padanya. e.text adalah apa yang diketik.

Untuk melakukan ini Kembalikan ini
Tulis ulang prompt. Transkrip dan riwayat prompt Anda menampilkan teks baru. next({ ...e, text: newText })
Tambahkan teks yang hanya dibaca Claude, setelah prompt next({ ...e, context: [...(e.context ?? []), extraText] })
Hentikan prompt agar tidak dikirim { drop: 'the reason' }

Hook ini menambahkan nama branch saat ini untuk Claude setiap kali prompt menyebutkan pull request:

on('prompt.submit', async ($, e, next) => {
  // Pass on a prompt that doesn't mention a pull request as it is
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Outside a git repository the command fails, so there's no branch to add
  if (git.exitCode !== 0) return next(e)
  // Keep any context an earlier hook added, and add one more line for 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 pull request diteruskan tanpa perubahan, dan git tidak berjalan.

Untuk menghentikan prompt, kembalikan { drop: 'the reason' } tanpa memanggil next. Teks tersebut kembali ke input prompt pengguna, dan mereka melihat Prompt dropped by a hook: diikuti alasan Anda, jadi tujukan alasan tersebut kepada mereka. Prompt tetap diteruskan dalam kasus berikut:

  • Hook Anda mengembalikan drop setelah panggilan next(e)-nya meloloskan prompt: giliran tetap berjalan, dan hook gagal dengan pesan yang menyertakan a drop after its next() was answered
  • Hook Anda melempar error atau melampaui batas waktu: Claude Code melewatinya dan mengirim prompt. Tambahkan handler .catch agar kegagalan tidak mengirim prompt yang seharusnya dihentikan oleh hook.

Peristiwa lain mencakup sisa apa yang dibaca Claude: prompt.section untuk setiap bagian system prompt, prompt.context untuk konteks yang dikirim dengan pesan pertama, dan skill.prompt untuk teks skill. Teks dari hook ini yang berubah antar permintaan membatalkan cache prompt. Dalam hook skill.prompt, Anda juga dapat membaca dari plugin atau file mana skill tersebut berasal.

Ikuti giliran

Giliran adalah semua yang dilakukan Claude sebagai jawaban atas satu prompt. Tangani turn.start, turn.step, dan turn.complete untuk mengikutinya:

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 tool memiliki beberapa permintaan. e.agentId ditetapkan untuk permintaan subagent. 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 subagent mengaktifkannya 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 ini mengalirkan data secara streaming. yield* next(e) meneruskan respons saat dialirkan dan menghasilkan hasil akhir. Hook ini mencatat berapa banyak dari setiap permintaan yang dilayani Claude API dari cache prompt:

// function* makes the hook a generator, which can pass the response on piece by piece
on('turn.step', async function* ($, e, next) {
  // Send the request, forward each piece as it arrives, and keep the finished result
  const result = yield* next(e)
  // Skip a result that reports no token counts
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Return the result unchanged, so the turn continues as usual
  return result
})

Respons Claude dialirkan 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 tool memiliki beberapa permintaan, sehingga menambahkan beberapa baris.

result.usage menyimpan 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 juga berjalan untuk permintaan subagent, jadi periksa e.agentId ketika Anda hanya menginginkan percakapan utama.

Untuk melihat panggilan tool yang dijalankan sendiri oleh API selama permintaan, seperti panggilan ke tool advisor, baca result.serverToolUses. Claude Code tidak menjalankan panggilan ini, sehingga tidak ada hook tool.call atau tool.check yang diaktifkan untuk panggilan tersebut. Bidang ini tidak ada ketika respons tidak memiliki panggilan semacam itu, dan memerlukan Claude Code v2.1.290 atau yang lebih baru.

Kirim permintaan ke model yang berbeda

Jika permintaan turn.step gagal dengan error model ketika model adalah alias seperti haiku, perbarui ke Claude Code v2.1.309 atau yang lebih baru, atau gunakan id model lengkap. Sebelum v2.1.309, alias dikirim apa adanya.

Untuk mengirim permintaan ke model yang berbeda, teruskan ke next peristiwa dengan model yang diubah, seperti dalam yield* next({ ...e, model: 'haiku' }).

Ubah atau tolak pembacaan pengaturan

Gunakan hook settings.read untuk mengubah atau menolak pembacaan pengaturan, baik pembacaan oleh mod maupun pembacaan oleh Claude Code sendiri:

  • Pembacaan oleh mod: mod memanggil $.settings.read.
  • Pembacaan oleh Claude Code sendiri: hook Anda pertama kali berjalan ketika Claude Code menggabungkan pengaturan sesi, setelah mod sesi dimuat. Hook Anda berjalan lagi ketika pengaturan atau plugin yang dimuat berubah.

Periksa mod mana yang mendapatkan peristiwa ini dan versi Claude Code Anda:

  • Penjaga bawaan: dengan pengaturan terkelola, atau dengan /login Team atau Enterprise sebagai kredensial sesi, penjaga bawaan dapat melewati hook ini dalam mod yang Anda instal atau muat dengan --plugin-dir. Untuk memeriksanya, cari settings.read bypassed by cc-plugin-sec-default di log debug. Jika baris dengan teks tersebut menyebutkan mod Anda, minta administrator organisasi Anda untuk menginstal mod tersebut sebagai salah satu mod organisasi.
  • Versi: peristiwa yang diaktifkan pada pembacaan Claude Code sendiri, dan bidang e.layers, memerlukan Claude Code v2.1.304 atau yang lebih baru.

Apa yang diterima hook settings.read

Bidang Tipe Isinya
e.layers array Satu objek { source, settings } untuk setiap sumber yang digunakan dan berisi pengaturan, mulai dari prioritas terendah: plugin, user, project, local, flag, policy. Kosong dalam claude plugin test.
e.source string Satu sumber yang diminta mod, seperti dalam $.settings.read({ source: 'policy' }). e.layers kemudian hanya berisi lapisan tersebut. Tidak ada pada pembacaan Claude Code sendiri dan pada pembacaan lain atas pengaturan gabungan.

Dalam e.layers, plugin berisi pengaturan yang disediakan plugin yang diaktifkan, dan hanya merupakan nama lapisan, bukan source yang dapat Anda teruskan ke $.settings.read. flag berisi pengaturan yang diteruskan dengan --settings, dan policy berisi pengaturan terkelola.

Bedakan pembacaan Claude Code sendiri dari pembacaan oleh mod

Periksa next.origin:

  • Pembacaan oleh Claude Code sendiri: { plugin: 'engine', tier: 'core' }
  • Pembacaan oleh mod: plugin adalah nama mod yang memanggil

Apa yang dapat dikembalikan hook settings.read

Kembalikan hasil next(e) tanpa perubahan untuk membiarkan pembacaan apa adanya, atau kembalikan salah satu dari berikut:

Nilai kembalian Pada pembacaan Claude Code sendiri Pada pembacaan oleh mod
{ value } Untuk setiap kunci yang diubah, Anda menggantikan seluruh nilainya dalam pengaturan gabungan sesi, jika nilai Anda sesuai dengan skema kunci Panggilan mod tersebut diselesaikan dengan value Anda, atau dengan key yang dimintanya yang diambil dari value Anda, jadi kembalikan objek utuh dan bukan array
{ deny: reason } Sesi mempertahankan pengaturan sebagaimana digabungkan sebelum hook mana pun menjawab, dan log debug mencatat reason Anda Panggilan mod tersebut ditolak dengan <calling mod>: $.settings.read: <reason>

Pada pembacaan Claude Code sendiri, susun { value } Anda dari value milik next(e), karena kunci yang Anda hilangkan menjadi tidak ditetapkan. Di tempat Claude Code membaca kunci seperti env dari setiap sumber secara terpisah, mengubahnya dalam value Anda tidak berpengaruh.

Apa yang diberikan next(e) kepada hook settings.read

next(e) diselesaikan dengan { value } atau dengan { deny }:

  • { value }: value adalah pengaturan gabungan, atau pengaturan satu sumber ketika e.source ditetapkan. Jika hook yang berjalan setelah hook Anda mengembalikan { value } miliknya sendiri, Anda mendapatkan value dari hook tersebut sebagai gantinya.
  • { deny }: hook yang berjalan setelah hook Anda menolak pembacaan. Kembalikan { deny } tersebut tanpa perubahan.

Ubah satu pengaturan dari hook settings.read

Pertama, periksa mod mana yang mendapatkan peristiwa ini, dan sejak versi berapa.

Untuk mengubah satu kunci, sebarkan value dari next(e) dan tetapkan kunci tersebut, seperti yang dilakukan hook ini dengan spinnerTipsEnabled. Hook tidak memeriksa next.origin, sehingga hook mengubah kunci tersebut baik pada pembacaan Claude Code sendiri maupun pada pembacaan gabungan oleh mod:

on('settings.read', async ($, e, next) => {
  const read = await next(e)
  // Pass through a read of one source, so that only the merged settings change.
  // Pass through a { deny } from a later hook too: it has no value.
  if (e.source || !read.value) return read
  // Keep every other key and change this one
  return { value: { ...read.value, spinnerTipsEnabled: false } }
})

Pada pembacaan Claude Code sendiri, jika hook Anda mengembalikan nilai di luar skema kunci, seperti string, hanya kunci tersebut yang mempertahankan nilai yang digabungkan sebelum hook mana pun menjawab. Log debug kemudian menyatakan the answer for <key> is no value the key can hold. Jika nilai Anda sesuai dan kunci tetap tidak berubah, lihat mengapa kunci tidak berlaku.

Tangani peristiwa hook pengaturan

Hook pengaturan adalah hook perintah, HTTP, prompt, dan agent 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 tersebut, seperti classic.Stop. e adalah JSON yang diterima hook pengaturan di stdin, termasuk transcript_path. Di tempat penjaga bawaan dimuat, hook pada peristiwa ini dalam mod yang Anda instal tidak berjalan.

Hook ini menggunakan Stop, yang diaktifkan ketika Claude selesai merespons, untuk mencatat lokasi penyimpanan transkrip sesi:

on('classic.Stop', async ($, e, next) => {
  // e has the same fields a Stop hook in a settings file reads from stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Pass the event on, so Stop hooks in your settings files still run
  return next(e)
})

Setiap kali Claude selesai merespons, baris redup dalam transkrip memberikan path file transkrip. Hook mengembalikan next(e), sehingga hook hanya mengamati peristiwa dan tidak mengubah apa pun tentang cara giliran berakhir.

Jalankan bersama mod lain

Beberapa mod dapat menangani peristiwa yang sama, dan salah satu dari mereka dapat gagal. Jika mod Anda memblokir panggilan tool, 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:

  1. Penjaga bawaan sec-default@builtin, mod yang dibangun ke dalam Claude Code yang /plugin daftarkan sebagai cc-plugin-sec-default, di mana dimuat, mod yang organisasi Anda daftarkan dalam prependPlugins, dan kemudian mod lain apa pun yang dihitung sebagai organisasi Anda dan tidak dalam appendPlugins
  2. Mod yang Anda instal
  3. Mod yang organisasi Anda daftarkan dalam appendPlugins
  4. 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 tool, pada titik tetap dalam rantai mod:

  • Hook PreToolUse dari pengaturan terkelola: berjalan sebelum hook tool.call mod pertama. Ketika hook terkelola memblokir panggilan, tidak ada hook tool.call mod yang berjalan untuk panggilan tersebut.
  • Hook PreToolUse dari setiap file pengaturan lain dan dari hooks/hooks.json plugin: berjalan setelah mod terakhir memanggil next, sebagai bagian dari perilaku Claude Code sendiri. Mod yang menjawab tool.call tanpa memanggil next mencegah mereka berjalan, dan mod yang memanggil next melihat keputusan mereka dalam hasil yang dikembalikan.

tool.check diaktifkan setelah hook PreToolUse berjalan. Untuk aturan izin dan keputusan hook PreToolUse yang tidak dapat digantikan oleh allow dari hook tool.check, lihat Perluas izin dengan hook.

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 next diselesaikan: 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 pohonnya tidak lolos validasi dihitung sebagai hook yang mengembalikan hasil dengan bentuk yang salah.

Untuk membuat hook yang memblokir panggilan gagal tertutup, tambahkan penanganan kesalahan .catch yang menjawab sebagai gantinya. Di sini, guard adalah fungsi hook Anda, dan penanganan menguji next.called untuk mengetahui apakah guard sudah memanggil next ketika gagal:

// on mengembalikan pendaftaran, dan .catch melampirkan penanganan ke hook itu saja
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // guard sudah memanggil next, jadi kembalikan apa yang diterima
  if (next.called) return next(e)
  // next.error.kind menyatakan mengapa penanganan dipanggil, seperti 'throw' atau 'timeout'
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Ketika guard melempar atau habis waktu pada panggilan Bash, Claude Code memanggil penanganan dengan peristiwa yang sama:

  • guard gagal sebelum memanggil next: perintah tidak berjalan, dan Claude membaca teks deny dengan jenis kegagalan di akhir
  • guard gagal setelah memanggil next: next(e) milik penanganan diselesaikan ke hasil yang dihasilkan oleh panggilan guard tanpa menjalankan perintah lagi, dan Claude membaca hasil tersebut

Penanganan memiliki batas waktu sendiri yang lebih singkat. Jika penanganan itu sendiri melempar atau habis waktu, Claude Code melewati hook seolah-olah hook tersebut tidak memiliki penanganan. Ketika guard belum memanggil next, perintah kemudian berlanjut seperti tanpa mod.

Bentuk penanganan yang sama cocok untuk penjaga pada prompt.submit atau config.set. Ketika next.called bernilai false, kembalikan penolakan yang tercantum dalam referensi peristiwa untuk peristiwa tersebut: { drop: 'the reason' } untuk prompt.submit, { deny: 'the reason' } untuk config.set.

Pada tool.check dan plugin.register, penolakan yang dikembalikan setelah next diselesaikan tetap berlaku, jadi kembalikan penolakan tersebut tanpa menguji next.called:

Pada tool.call, deny yang dikembalikan setelah next diselesaikan menahan hasil dari Claude.

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