SpyBara
Go Premium

plugins/mods/events.md 2026-10-01 23:59 UTC to 2026-10-02 18:59 UTC

This page contains 90 additions and 63 deletions.

2026
Thu 1 23:59 Fri 2 19:58

Reagire agli eventi con un mod

Gestire gli eventi di Claude Code da un mod: osservare, riscrivere o rispondere alle chiamate di strumenti, ai prompt e ai turni, filtrare quali eventi un hook gestisce e pianificare per altri mod.

Un hook è un gestore di eventi: una funzione che Claude Code esegue quando si verifica un evento denominato. Claude Code genera un evento in ogni punto in cui sta per agire, ad esempio quando esegue uno strumento, invia un prompt, invia una richiesta al modello o avvia o termina una sessione. Il vostro hook viene eseguito prima che Claude Code agisca, quindi può osservare l'evento, riscriverlo o rispondere al suo posto. Registrate un hook con on(eventName, handler).

Costruite il vostro primo mod prima di iniziare qui. Per ogni evento e i suoi campi esatti, consultate il riferimento o leggete i tipi per la vostra build.

Come un hook gestisce un evento

Un hook si trova tra un evento e ciò che Claude Code farebbe al riguardo, quindi può osservare l'evento, riscriverlo o rispondere da solo. Riceve tre argomenti: l'API dei mods come $, l'evento come e e il gestore successivo come next. I gestori di un evento formano una catena middleware. next(e) chiama il gestore successivo, che è un hook di un altro mod o, alla fine della catena, il comportamento proprio di Claude Code, e si risolve nel risultato. Ciò che il vostro hook fa con next decide quale dei tre fa.

Osservare un evento

Per osservare un evento senza modificarlo, fate il vostro lavoro e restituite next(e). Questo hook registra ogni strumento che Claude sta per utilizzare:

on('tool.call', async ($, e, next) => {
  // Viene eseguito prima che lo strumento venga eseguito
  $.ui.log('Claude is about to use ' + e.tool)
  // Passa l'evento invariato
  return next(e)
})

Prima di ogni esecuzione dello strumento, una riga attenuata come ● my-mod: Claude is about to use Bash appare nella trascrizione, dove my-mod è il nome del vostro plugin. Lo strumento viene eseguito come farebbe senza il mod.

Per agire dopo l'evento, await next(e), fate il vostro lavoro e restituite il risultato. Questo hook registra ogni strumento dopo che è stato eseguito:

on('tool.call', async ($, e, next) => {
  // Lascia che lo strumento venga eseguito e attendi il suo risultato
  const result = await next(e)
  // Viene eseguito dopo che lo strumento è stato eseguito
  $.ui.log(e.tool + ' finished')
  // Restituisci il risultato invariato
  return result
})

La riga ora appare dopo che ogni strumento termina. Claude legge lo stesso risultato in entrambi i casi, perché l'hook restituisce ciò a cui next(e) si è risolto.

Riscrivere un evento

Per modificare ciò su cui Claude Code agisce, ad esempio il testo di un prompt, chiama next con una copia modificata dell'evento. L'evento stesso è immutabile: è congelato a ogni profondità e l'assegnazione a un campo genera un errore. Questo hook taglia ogni prompt prima che venga inviato:

on('prompt.submit', async ($, e, next) => {
  // Passa una copia dell'evento con il suo testo modificato
  return next({ ...e, text: e.text.trim() })
})

I gestori successivi e Claude Code ricevono il prompt tagliato e non vedono mai l'originale. Potete anche modificare il risultato: await next(e), quindi restituite una copia del risultato con un campo sostituito.

Rispondere a un evento

Per gestire un evento da soli, restituite un risultato senza chiamare next. Questo cortocircuita la catena, quindi i mod successivi e il comportamento proprio di Claude Code non vengono eseguiti. Questo hook rifiuta ogni comando Bash:

on('tool.call', { tool: 'Bash' }, async () => {
  // Nessuna chiamata a next, quindi il comando non viene mai eseguito
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Quando Claude prova un comando Bash, il comando non viene eseguito e Claude legge il testo deny come risultato dello strumento. Ogni evento ha la sua forma di risultato, che il riferimento degli eventi elenca.

Filtrare quali eventi un hook gestisce

Per eseguire un hook solo per alcuni eventi, passate un filtro come secondo argomento a on. Claude Code chiama il filtro un matcher. È un oggetto i cui campi vengono confrontati con quelli dell'evento e l'hook viene eseguito solo quando ogni campo corrisponde. Un campo può essere un valore, un array di valori consentiti o un'espressione regolare.

Ogni riga in questo esempio registra la stessa funzione, hook, per un insieme più ristretto di chiamate di strumenti:

// Una stringa corrisponde a un valore: solo chiamate Bash
on('tool.call', { tool: 'Bash' }, hook)
// Un array corrisponde a qualsiasi valore in esso: chiamate Edit e Write
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// Un'espressione regolare corrisponde per pattern: ogni strumento di un server MCP
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook viene eseguito una volta per una chiamata Bash, Edit o Write e una volta per una chiamata a uno strumento il cui nome inizia con mcp__github__. Una chiamata a qualsiasi altro strumento, come Read, non corrisponde a nessuno dei tre, quindi hook non viene eseguito per essa.

Il nome dell'evento può essere un wildcard. 'classic.*' corrisponde a ogni evento hook delle impostazioni. '*' corrisponde a ogni evento tranne gli eventi di telemetria, che richiedono il proprio nome e un filtro { to: 'collector' }.

Registrate ogni evento una volta per matcher. Se chiamate on due volte per session.start senza un matcher, il modulo non si carica con on("session.start") is registered twice without a matcher. Mettete tutto ciò che il vostro mod fa all'inizio della sessione in un hook.

Agganciarsi a ciò che Claude sta facendo

Gestisci questi eventi per vedere o modificare una chiamata a uno strumento, un prompt o un turno mentre avviene. Per ogni evento e per ciò che un hook può restituire, consulta il riferimento degli eventi.

Controllare o modificare una chiamata a uno strumento

Un hook tool.call vede ogni strumento che Claude sta per usare, quindi può rifiutare la chiamata, modificarne gli argomenti o lasciarla passare. tool.call si attiva quando Claude Code sta per eseguire uno strumento, incluse le chiamate effettuate da un subagent e le chiamate agli strumenti MCP. e.tool è il nome dello strumento e gli argomenti dello strumento sono campi di e, come e.command per Bash. Quando chiami next(e), Claude Code esegue il controllo dei permessi e poi lo strumento.

Questo hook rifiuta un comando Bash che esegue un force push e ne spiega il motivo a Claude:

// 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)
})

Quando Claude prova git push --force, il comando non viene eseguito e non compare alcuna richiesta di permesso, perché l'hook non chiama mai next. Claude legge il testo di deny come risultato dello strumento, quindi scrivilo come un'istruzione su cui Claude possa agire. Ogni altro comando Bash viene eseguito come farebbe senza la mod.

Per agire dopo che uno strumento è stato eseguito, usa await next(e), svolgi il tuo lavoro e restituisci ciò che next ti ha fornito. Questo hook registra ogni file .mdx che Claude modifica, con $.ui.log, che aggiunge alla trascrizione una riga attenuata che Claude non legge:

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
})

Dopo che Claude modifica o scrive un file .mdx, una riga attenuata nella trascrizione indica il nome del file. Non viene registrato nulla per un altro tipo di file, né per una chiamata rifiutata o non riuscita. La visione che Claude ha della chiamata non cambia, perché l'hook restituisce il risultato che ha ricevuto.

Per modificare una chiamata, passa gli argomenti modificati a next. Per riprovare una chiamata, chiama di nuovo next(e): un hook che vede isError nel primo risultato può eseguire lo strumento una seconda volta e restituire quel risultato. Per rispondere tu stesso a una chiamata, restituisci un oggetto con un campo result, come { result: 'Skipped by my-mod' }, senza chiamare next. In questo caso non compare alcuna richiesta di permesso e lo strumento non viene eseguito, quindi il risultato che restituisci è tutto ciò che Claude sa di quanto è accaduto.

Gli hook nelle impostazioni gestite della tua organizzazione vengono eseguiti prima dell'hook tool.call di qualsiasi mod, e un blocco da parte di uno di essi è definitivo.

Trattenere una chiamata a uno strumento finché l'utente non decide

Un hook può mettere in pausa una chiamata a uno strumento e chiedere all'utente cosa fare prima che proceda. Un hook tool.call può usare await prima di chiamare next o di restituire un valore, e fino ad allora la chiamata allo strumento resta in sospeso. Per porre la domanda all'utente, chiama $.ui.ask. Mostra la tua domanda sopra un elenco numerato delle tue opzioni, nella finestra di dialogo che Claude usa per chiederti qualcosa, e si risolve con l'etichetta scelta dall'utente. Dopo le tue opzioni, la finestra di dialogo aggiunge una riga per digitare una risposta diversa e una riga Chat about this.

Il pattern RISKY in questo esempio corrisponde a rm -r, rm -rf, git reset --hard e git push con --force, e non rileva altre varianti come git push -f. Questo modulo chiede conferma prima di eseguire un comando Bash che corrisponde al pattern:

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)
  })
}

Quando Claude prova un comando come rm -rf build, compare la domanda contenente il comando, e il comando attende la risposta:

  • L'utente sceglie Run it: l'hook chiama next(e), e dopo viene comunque eseguito il consueto controllo dei permessi
  • L'utente sceglie Refuse: il comando non viene eseguito e Claude legge il testo di deny
  • L'utente digita una risposta: $.ui.ask si risolve con il testo digitato. L'hook lo confronta con Run it, quindi qualsiasi altro testo rifiuta il comando.
  • Nessuno risponde: $.ui.ask viene rifiutata quando l'utente chiude la domanda o sceglie Chat about this, e in un'esecuzione claude -p, quindi il blocco catch lascia la risposta a Refuse

Mantieni l'attesa all'interno di una chiamata all'API delle mod come $.ui.ask, perché quel tempo non viene conteggiato nel limite di tempo dell'hook. Il tempo trascorso in attesa di una tua promise invece viene conteggiato. Claude Code salta un hook che va in timeout, quindi il comando trattenuto verrebbe eseguito.

Approvare o rifiutare una chiamata a uno strumento prima che venga chiesto all'utente

Per decidere se una chiamata a uno strumento può essere eseguita, gestisci tool.check, l'evento in cui Claude Code prende questa decisione. Si attiva dopo che le regole di permesso e gli hook delle impostazioni hanno deciso, e next(e) si risolve con la loro decisione: allow, ask o deny. Il tuo hook restituisce quella decisione o una diversa. e.input contiene gli argomenti dello strumento, come command per Bash.

Per un comando o un percorso fisso, usa una regola di permesso come Bash(npm test), che non richiede codice. Gestisci tool.check quando la decisione dipende da ciò che è vero in quel momento, come il branch Git corrente o un valore registrato da un altro hook.

Questo hook rifiuta git push mentre il branch corrente è main:

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or 'deny'
  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' }
})

Su main, l'hook restituisce deny, anche quando una regola consente git push. Su un altro branch, e per altri comandi, la chiamata ottiene la decisione che otterrebbe senza la mod.

L'hook confronta il testo del comando, quindi consideralo un promemoria per Claude. Per bloccare i push su main per tutti, proteggi il branch sul tuo host Git.

Un hook può restituire allow, ask o deny, quindi può anche approvare una chiamata che un hook PreToolUse esterno alle impostazioni gestite ha bloccato. Estendere i permessi con gli hook elenca quali decisioni prevalgono su una mod.

Riscrivere o integrare un prompt

Un hook prompt.submit vede ogni prompt prima che inizi il turno, quindi può riscriverne il testo o aggiungervi qualcosa. e.text è ciò che è stato digitato.

Per fare questo Restituisci questo
Riscrivere il prompt. Il messaggio nella trascrizione mostra il nuovo testo. next({ ...e, text: newText })
Aggiungere, dopo il prompt, testo che solo Claude legge next({ ...e, context: [...(e.context ?? []), extraText] })
Impedire l'invio del prompt { drop: 'the reason' }

Questo hook aggiunge per Claude il nome del branch corrente ogni volta che un prompt menziona una 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()] })
})

Quando invii un prompt come open a PR for this change, il tuo messaggio appare invariato nella trascrizione, e Claude legge anche una riga come Current branch: feature/auth dopo di esso. Un prompt che non menziona una pull request passa invariato e git non viene eseguito.

Altri eventi coprono il resto di ciò che Claude legge: prompt.section per ogni sezione del prompt di sistema, prompt.context per il contesto inviato con il primo messaggio e skill.prompt per il testo di una skill. Il testo di questi hook che cambia tra una richiesta e l'altra invalida la cache dei prompt.

Seguire un turno

Un turno è tutto ciò che Claude fa in risposta a un prompt. Gestisci turn.start, turn.step e turn.complete per seguirne uno:

Evento Quando si attiva Cosa può fare un hook
turn.start Inizia un turno Osservare. e.turnId identifica il turno negli altri due eventi.
turn.step Claude Code sta per inviare una richiesta al modello. Un turno con chiamate agli strumenti ne ha diverse. e.agentId è impostato per la richiesta di un subagent. Leggere l'utilizzo di token di ogni richiesta, inviarla a un modello diverso con next({ ...e, model }) o rispondere senza chiamare il modello
turn.complete Il turno è terminato, incluso un turno interrotto dall'utente, nel qual caso e.isAborted è true. e.answer è il testo finale di Claude, e.durationMs la sua durata ed e.usage i totali di token del turno. Il turno di un subagent lo attiva con e.agentId impostato. Osservare, oppure restituire un oggetto con un campo text, come { text: 'Done in 12 seconds' }, per mostrare una riga sotto la risposta

Scrivi un hook turn.step come generatore asincrono, perché l'evento è in streaming. yield* next(e) inoltra la risposta durante lo streaming e restituisce il risultato finale. Questo hook registra quanta parte di ogni richiesta la Claude API ha servito dal prompt caching:

// 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
})

La risposta di Claude viene trasmessa sullo schermo come avviene senza la mod. Al termine di ogni richiesta, una riga attenuata nella trascrizione indica il numero di token letti dalla cache e il numero di quelli scritti in essa. Un turno con chiamate agli strumenti ha diverse richieste, quindi aggiunge diverse righe.

result.usage contiene i conteggi di token che la Claude API riporta per una richiesta, più il model che ha risposto: input_tokens, output_tokens, cache_read_input_tokens e cache_creation_input_tokens. L'hook viene eseguito anche per le richieste dei subagent, quindi controlla e.agentId quando vuoi solo la conversazione principale.

Gestire gli eventi degli hook delle impostazioni

Gli hook delle impostazioni sono gli hook di tipo command, HTTP, prompt e agent che configuri nei file di impostazioni. Ogni evento degli hook delle impostazioni, come Stop, SessionEnd o PostToolUse, è anche un evento denominato classic. seguito dal nome dell'evento dell'hook delle impostazioni, come classic.Stop. e è il JSON che un hook delle impostazioni riceve su stdin, incluso transcript_path.

Questo hook usa Stop, che si attiva quando Claude termina di rispondere, per registrare dove viene salvata la trascrizione della sessione:

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)
})

Ogni volta che Claude termina di rispondere, una riga attenuata nella trascrizione indica il percorso del file della trascrizione. L'hook restituisce next(e), quindi osserva l'evento senza cambiare nulla nel modo in cui termina il turno.

Eseguire insieme ad altri mod

Diversi mod possono gestire lo stesso evento e uno qualsiasi di essi può fallire. Se il tuo mod blocca le chiamate agli strumenti, controlla la sua posizione nella catena e cosa accade quando il suo hook fallisce.

L'ordine in cui i mod vengono eseguiti

Gli hook sullo stesso evento formano una catena middleware. Ogni next di un mod chiama l'hook del mod seguente e l'ultimo next raggiunge il comportamento proprio di Claude Code. Il primo mod è il più esterno: vede l'evento prima degli altri e il risultato dopo di loro, e decide se gli altri vengono eseguiti. Un mod successivo non può impedire a uno precedente di vedere un evento.

Claude Code ordina la catena in base a dove proviene ogni mod:

  1. La guardia incorporata sec-default@builtin, un mod incorporato in Claude Code che /plugin elenca come cc-plugin-sec-default, dove si carica, i mod che la vostra organizzazione elenca in prependPlugins e quindi qualsiasi altro mod che conta come della vostra organizzazione e non è in appendPlugins
  2. I mod che installate
  3. I mod che la vostra organizzazione elenca in appendPlugins
  4. Altri mod incorporati in Claude Code

Tra i mod che installate, un mod viene eseguito prima dei mod che elenca sotto dependencies nel suo manifesto. All'interno di un modulo, gli hook vengono eseguiti nell'ordine in cui register ha chiamato on.

Dove gli hook delle impostazioni vengono eseguiti nell'ordine

Gli hook PreToolUse configurati nei file delle impostazioni vengono anche eseguiti durante una chiamata di strumento, in punti fissi nella catena dei mod:

  • Hook PreToolUse dalle impostazioni gestite: vengono eseguiti prima dell'hook tool.call del primo mod, e un blocco da uno di essi è definitivo, quindi nessun mod vede la chiamata.
  • Hook PreToolUse da ogni altro file di impostazioni e da hooks/hooks.json dei plugin: vengono eseguiti dopo che l'ultimo mod chiama next, come parte del comportamento proprio di Claude Code. Un mod che risponde a tool.call senza chiamare next li impedisce di eseguire e un mod che chiama next vede la loro decisione nel risultato che restituisce.

tool.check si attiva dopo che quegli hook e le regole di permesso hanno deciso, quindi un hook su di esso può approvare una chiamata che un hook nel secondo gruppo ha bloccato.

Gestire un hook che fallisce

Un hook che fallisce non interrompe la sessione e potete decidere cosa accade invece. Quando un hook senza un gestore .catch genera un errore, scade o restituisce un risultato di forma sbagliata, ciò che accade dopo dipende dal fatto che abbia chiamato next:

  • Ha fallito prima di chiamare next: Claude Code lo salta e il gestore successivo viene eseguito al suo posto
  • Ha fallito dopo che next si è risolto: quel risultato rimane e nulla viene eseguito una seconda volta

Una riga nomina il mod, l'evento e il motivo, come my-mod: tool.call hook skipped: threw Error: boom. Dove lo leggete dipende dalla sessione, come Scoprire perché un mod non fa nulla elenca. Un hook ui.render il cui disegno non convalida viene segnalato diversamente, come Costruire un albero da elementi descrive.

Per fare in modo che un hook che blocca le chiamate fallisca in modo sicuro, aggiungete un gestore di errore .catch che risponda al suo posto. Qui, guard è la vostra funzione hook:

// on restituisce una registrazione e .catch allega un gestore a quel singolo hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind è 'throw' o 'timeout', che dice come guard ha fallito
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Mentre guard funziona, il gestore non viene mai eseguito. Quando guard genera un errore o scade su una chiamata Bash, Claude Code chiama il gestore con lo stesso evento. Il gestore restituisce { deny }, quindi il comando non viene eseguito e Claude legge il testo con throw o timeout alla fine. Senza il gestore, Claude Code salterebbe guard ed eseguirebbe il comando. Il gestore ha un proprio limite di tempo più breve.

Passaggi successivi