SpyBara
Go Premium

plugins/mods/events.md 2026-11-07 23:58 UTC to 2026-11-08 22:58 UTC

No changes for this document in this history range.

2026
Mon 9 23:02 Tue 10 21:57

Reagire agli eventi con un mod

Gestisci gli eventi di Claude Code da un mod: osserva, riscrivi o rispondi a chiamate agli strumenti, prompt e turni, filtra quali eventi gestisce un hook e pianifica in vista di altri mod.

Un hook è un gestore di eventi: una funzione che Claude Code esegue quando si verifica un evento con un determinato nome. 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 oppure avvia o termina una sessione. Il tuo hook viene eseguito prima che Claude Code agisca, quindi può osservare l'evento, riscriverlo o rispondere al posto di Claude Code. Registri un hook con on(eventName, handler).

Crea il tuo primo mod prima di iniziare da qui. Per ogni evento e i relativi campi esatti, consulta il riferimento oppure leggi i tipi per la tua build.

Come un hook gestisce un evento

Un hook si colloca tra un evento e ciò che Claude Code farebbe in risposta, quindi può osservare l'evento, riscriverlo o rispondere direttamente. Riceve tre argomenti: l'API dei mod 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 è l'hook di un altro mod oppure, alla fine della catena, il comportamento nativo di Claude Code, e si risolve nel risultato. Ciò che il tuo hook fa con next determina quale delle tre azioni compie.

Osservare un evento

Per osservare un evento senza modificarlo, esegui il tuo lavoro e restituisci next(e). Questo hook registra nel log ogni strumento che Claude sta per usare:

on('tool.call', async ($, e, next) => {
  // Runs before the tool does
  $.ui.log('Claude is about to use ' + e.tool)
  // Pass the event on unchanged
  return next(e)
})

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

Per agire dopo l'evento, esegui await next(e), svolgi il tuo lavoro e restituisci il risultato. Questo hook registra nel log ogni strumento dopo la sua esecuzione:

on('tool.call', async ($, e, next) => {
  // Let the tool run, and wait for its result
  const result = await next(e)
  // Runs after the tool does
  $.ui.log(e.tool + ' finished')
  // Give the result back unchanged
  return result
})

Ora la riga compare dopo il termine di ogni strumento. Claude legge lo stesso risultato in entrambi i casi, perché l'hook restituisce ciò in cui si è risolto next(e).

Riscrivere un evento

Per cambiare ciò su cui Claude Code agisce, come il testo di un prompt, chiama next con una copia modificata dell'evento. L'evento stesso è immutabile: è congelato in profondità e l'assegnazione a un campo genera un'eccezione. Questo hook rimuove gli spazi iniziali e finali da ogni prompt prima che venga inviato:

on('prompt.submit', async ($, e, next) => {
  // Pass on a copy of the event with its text changed
  return next({ ...e, text: e.text.trim() })
})

I gestori successivi e Claude Code ricevono il prompt ripulito e non vedono mai l'originale. Puoi anche modificare il risultato: esegui await next(e), poi restituisci una copia del risultato con un campo sostituito.

Rispondere a un evento

Per gestire tu stesso un evento, restituisci un risultato senza chiamare next. In questo modo interrompi la catena, quindi i mod successivi e il comportamento nativo di Claude Code non vengono eseguiti. Questo hook rifiuta ogni comando Bash:

on('tool.call', { tool: 'Bash' }, async () => {
  // No call to next, so the command never runs
  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 di deny come risultato dello strumento. Ogni evento ha una propria forma del risultato, elencata nel riferimento degli eventi.

Filtrare gli eventi gestiti da un hook

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

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

// A string matches one value: Bash calls only
on('tool.call', { tool: 'Bash' }, hook)
// An array matches any value in it: Edit calls and Write calls
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// A regular expression matches by pattern: every tool of one MCP server
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook viene eseguita una volta per una chiamata a 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 eseguita.

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

Registra ogni evento una sola volta per matcher. Se chiami on due volte per session.start senza matcher, il caricamento del modulo fallisce con on("session.start") is registered twice without a matcher. Inserisci tutto ciò che il tuo mod fa all'avvio della sessione in un unico hook.

Agganciarsi a ciò che Claude sta facendo

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

Proteggere 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 spiega a Claude il motivo:

// 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 il mod.

Se questo hook genera un'eccezione o va in timeout, Claude Code lo salta e il comando procede come farebbe senza il mod. Per rifiutare invece il comando, aggiungi un handler .catch all'hook.

Per agire dopo che uno strumento è stato eseguito, usa await next(e), svolgi il tuo lavoro e restituisci ciò che next ti ha dato. 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.

Il tuo hook può anche modificare una chiamata, riprovarla, rispondervi tu stesso o nasconderne il risultato:

  • Modificare la chiamata: passa argomenti modificati a next.
  • Riprovare la 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.
  • Rispondere tu stesso alla chiamata: restituisci un oggetto con un campo result, senza chiamare next, e per uno strumento integrato dai a result la forma che ha il risultato di quello strumento nei tipi per la tua build. 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.
  • Nascondere il risultato a Claude: restituisci { deny: reason } dopo await next(e). Claude legge il tuo motivo al posto di ciò che next ha restituito. Quando lo strumento è stato eseguito, il deny impedisce a Claude di vederne il risultato e non annulla nulla di ciò che lo strumento ha fatto. Quando lo strumento è stato eseguito con successo, il motivo segue una nota come Bash ran, and a plugin withheld its result:.

Gli hook nelle impostazioni gestite della tua organizzazione vengono eseguiti prima dell'hook tool.call di qualsiasi mod. Quando un hook gestito blocca la chiamata, nessun hook tool.call di alcun mod viene eseguito per quella chiamata.

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 la chiamata allo strumento resta in sospeso fino ad allora. 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 nell'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 coglie altre forme 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 con il comando al suo interno, e il comando attende la risposta:

  • L'utente sceglie Run it: l'hook chiama next(e), e il consueto controllo dei permessi viene comunque eseguito dopo
  • 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 nel 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 dei 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. Per rifiutarlo invece, concatena un handler .catch alla registrazione restituita da on. Quell'handler è separato dal blocco catch all'interno dell'hook.

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.

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.

  • Cosa restituire: per decidere tu stesso la chiamata, restituisci un oggetto tuo, come { decision: 'allow' } o { decision: 'deny', reason }. Per lasciare invariata la decisione, restituisci ciò che ti dà await next(e).
  • Cosa ti dà await next(e): la decisione di Claude Code stesso, come oggetto il cui decision è allow, ask o deny. Non compare alcuna richiesta di permesso mentre next(e) è in esecuzione.
  • Argomenti: e.input contiene gli argomenti dello strumento, come command per Bash
  • Quando si attiva tool.check: dopo l'esecuzione degli hook PreToolUse, e prima della richiesta di permesso, del classificatore della modalità auto o di un hook PermissionRequest

Questo hook rifiuta git push mentre il branch corrente è 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' }
})

L'hook rifiuta git push su main e lascia passare ogni altra chiamata:

  • Su main: l'hook restituisce deny, anche quando una regola consente git push
  • Su un altro branch, e per altri comandi: la chiamata riceve la decisione che riceverebbe senza il mod
  • L'hook genera un'eccezione o va in timeout: anche in questo caso la chiamata riceve la decisione che riceverebbe senza il mod. Per rifiutare invece la chiamata, aggiungi un handler .catch che restituisca { decision: 'deny', reason: 'the reason' }.

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.

[!NOTE]

Un hook tool.check può anche restituire allow o ask. In plan mode, per una chiamata che non è di sola lettura, come la modifica di un file, il tuo hook può rendere più restrittiva la decisione restituita da next(e) ma non può allentarla. Per le regole di permesso e le decisioni degli hook PreToolUse che l'allow del tuo hook non può sostituire, consulta Estendere i permessi con gli hook.

Verificare se una chiamata a uno strumento è di sola lettura

In un hook tool.check, e.isReadOnly è true quando lo strumento segnala la chiamata come di sola lettura. Altrimenti il campo è assente, non false. e.isReadOnly richiede Claude Code v2.1.304 o successivo.

Alcuni strumenti, come Read, Grep, Glob e WebSearch, segnalano ogni chiamata come di sola lettura. Bash e gli strumenti MCP segnalano una chiamata come di sola lettura in questi casi:

Il valore è ciò che segnala lo strumento stesso, quindi non approvare una chiamata basandoti solo su questo campo. Ad esempio, le chiamate WebFetch e Agent vengono segnalate come di sola lettura, anche se WebFetch richiede un URL e Agent avvia un subagent che esegue strumenti propri.

Questo hook restituisce un ask per ogni chiamata mcp__github__ che non è di sola lettura e non è già stata rifiutata:

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

Filtrare un hook tool.check in base al fatto che la chiamata sia di sola lettura

Per limitare le chiamate gestite dal tuo hook in base a e.isReadOnly, usa il matcher per le chiamate di sola lettura e il corpo dell'hook per le altre:

  • Solo chiamate di sola lettura: aggiungi isReadOnly: true al matcher, come in on('tool.check', { tool: 'Bash', isReadOnly: true }, hook)
  • Chiamate che non sono di sola lettura: verifica e.isReadOnly nel corpo dell'hook, come fa l'esempio mcp__github__. Se metti isReadOnly: false in un matcher, il tuo hook non viene mai eseguito.

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. La trascrizione e la tua cronologia dei prompt mostrano il nuovo testo. next({ ...e, text: newText })
Aggiungere testo, dopo il prompt, 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.

Per bloccare un prompt, restituisci { drop: 'the reason' } senza chiamare next. Il testo torna nel campo di input del prompt dell'utente, che vede Prompt dropped by a hook: seguito dal tuo motivo, quindi rivolgi il motivo all'utente. Il prompt passa comunque in questi casi:

  • Il tuo hook restituisce un drop dopo che la sua chiamata a next(e) ha lasciato passare il prompt: il turno viene comunque eseguito e l'hook non riesce con un messaggio che include a drop after its next() was answered
  • Il tuo hook genera un'eccezione o va in timeout: Claude Code lo salta e invia il prompt. Aggiungi un handler .catch in modo che un errore non invii un prompt che l'hook avrebbe bloccato.

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. In un hook skill.prompt puoi anche leggere da quale plugin o file proviene la skill.

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 quanto tempo ha richiesto ed e.usage i totali di token del turno. Il turno di un subagent lo attiva con e.agentId impostato. Osservare, o 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 mentre arriva in streaming e restituisce il risultato finale. Questo hook registra quanto di ogni richiesta la Claude API ha servito dalla cache dei 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
})

La risposta di Claude viene mostrata sullo schermo in streaming come accade senza il 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.

Per vedere le chiamate agli strumenti che l'API ha eseguito autonomamente durante la richiesta, come le chiamate allo strumento advisor, leggi result.serverToolUses. Claude Code non esegue queste chiamate, quindi nessun hook tool.call o tool.check si attiva per esse. Il campo è assente quando la risposta non contiene chiamate di questo tipo e richiede Claude Code v2.1.290 o successivo.

Inviare una richiesta a un modello diverso

Se una richiesta turn.step non riesce con un errore del modello quando model è un alias come haiku, aggiorna a Claude Code v2.1.309 o successivo, oppure usa un id di modello completo. Prima della v2.1.309, un alias veniva inviato così come scritto.

Per inviare una richiesta a un modello diverso, passa a next l'evento con model modificato, come in yield* next({ ...e, model: 'haiku' }).

Modificare o rifiutare una lettura delle impostazioni

Usa un hook settings.read per modificare o rifiutare una lettura delle impostazioni, sia quella di un mod sia quella di Claude Code stesso:

  • La lettura di un mod: un mod chiama $.settings.read.
  • La lettura di Claude Code stesso: il tuo hook viene eseguito per la prima volta quando Claude Code unisce le impostazioni della sessione, dopo che i mod della sessione sono stati caricati. Il tuo hook viene eseguito di nuovo quando cambiano le impostazioni o i plugin caricati.

Verifica quali mod ricevono questo evento e la tua versione di Claude Code:

  • La protezione integrata: con le impostazioni gestite, o con un /login Team o Enterprise come credenziale della sessione, la protezione integrata può saltare questo hook in un mod che installi o carichi con --plugin-dir. Per verificarlo, cerca nel log di debug settings.read bypassed by cc-plugin-sec-default. Se una riga con quel testo nomina il tuo mod, chiedi all'amministratore della tua organizzazione di installare il mod come uno dei mod dell'organizzazione.
  • La versione: l'attivazione dell'evento sulla lettura di Claude Code stesso, e il campo e.layers, richiedono Claude Code v2.1.304 o successivo.

Cosa riceve un hook settings.read

Campo Tipo Cosa contiene
e.layers array Un oggetto { source, settings } per ogni origine in uso che contiene impostazioni, a partire da quella con precedenza più bassa: plugin, user, project, local, flag, policy. Vuoto in claude plugin test.
e.source string L'unica origine richiesta da un mod, come in $.settings.read({ source: 'policy' }). In tal caso e.layers contiene solo quel livello. Assente nella lettura di Claude Code stesso e in qualsiasi altra lettura delle impostazioni unite.

In e.layers, plugin contiene le impostazioni fornite dai plugin abilitati ed è solo il nome di un livello, non una source che puoi passare a $.settings.read. flag contiene le impostazioni passate con --settings, e policy contiene le impostazioni gestite.

Distinguere la lettura di Claude Code stesso dalla lettura di un mod

Controlla next.origin:

  • La lettura di Claude Code stesso: { plugin: 'engine', tier: 'core' }
  • La lettura di un mod: plugin è il nome del mod chiamante

Cosa può restituire un hook settings.read

Restituisci invariato il risultato di next(e) per lasciare la lettura così com'è, oppure restituisci uno di questi:

Valore restituito Nella lettura di Claude Code stesso Nella lettura di un mod
{ value } Per ogni chiave modificata, sostituisci il suo intero valore nelle impostazioni unite della sessione, se il tuo rispetta lo schema della chiave La chiamata di quel mod si risolve nel tuo value, oppure nella key richiesta presa dal tuo value, quindi restituisci un oggetto intero e non un array
{ deny: reason } La sessione mantiene le impostazioni così come erano state unite prima che qualsiasi hook rispondesse, e il log di debug registra il tuo reason La chiamata di quel mod viene rifiutata con <calling mod>: $.settings.read: <reason>

Nella lettura di Claude Code stesso, costruisci il tuo { value } a partire dal value di next(e), poiché una chiave che ometti risulta non impostata. Quando Claude Code legge una chiave come env da ciascuna origine separatamente, modificarla nel tuo value non ha effetto.

Cosa fornisce next(e) a un hook settings.read

next(e) si risolve in { value } o in { deny }:

  • { value }: value sono le impostazioni unite, oppure le impostazioni di una sola origine quando e.source è impostato. Se un hook che viene eseguito dopo il tuo ha restituito un { value } proprio, ricevi invece il value di quell'hook.
  • { deny }: un hook eseguito dopo il tuo ha rifiutato la lettura. Restituisci quel { deny } invariato.

Modificare un'impostazione da un hook settings.read

Verifica prima quali mod ricevono questo evento e da quale versione.

Per modificare una chiave, espandi il value di next(e) e imposta quella chiave, come fa questo hook con spinnerTipsEnabled. L'hook non controlla next.origin, quindi modifica la chiave sia nella lettura di Claude Code stesso sia nella lettura unita di un 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 } }
})

Nella lettura di Claude Code stesso, se il tuo hook restituisce un valore al di fuori dello schema della chiave, come una stringa, solo quella chiave mantiene il valore unito prima che qualsiasi hook rispondesse. Il log di debug riporta quindi the answer for <key> is no value the key can hold. Se il tuo valore è valido e la chiave comunque non cambia, consulta perché una chiave non ha effetto.

Gestire gli eventi degli hook delle impostazioni

Gli hook delle impostazioni sono gli hook di comando, HTTP, prompt e agente 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. Dove viene caricata la protezione integrata, un hook su questi eventi in un mod che installi non viene eseguito.

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 e non modifica nulla del modo in cui termina il turno.

Eseguire insieme ad altri mod

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

L'ordine in cui vengono eseguiti i mod

Gli hook sullo stesso evento formano un'unica catena middleware. Il next di ciascun mod chiama l'hook del mod successivo, 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 o meno. Un mod successivo non può impedire a uno precedente di vedere un evento.

Claude Code ordina la catena in base alla provenienza di ciascun mod:

  1. La protezione integrata sec-default@builtin, un mod integrato in Claude Code che /plugin elenca come cc-plugin-sec-default, dove viene caricata, i mod che la tua organizzazione elenca in prependPlugins, e poi qualsiasi altro mod che conta come della tua organizzazione e non è in appendPlugins
  2. I mod che installi tu
  3. I mod che la tua organizzazione elenca in appendPlugins
  4. Gli altri mod integrati in Claude Code

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

Dove vengono eseguiti gli hook delle impostazioni nell'ordine

Anche gli hook PreToolUse configurati nei file di impostazioni vengono eseguiti durante una chiamata a uno strumento, in punti fissi della catena di mod:

  • Hook PreToolUse dalle impostazioni gestite: vengono eseguiti prima dell'hook tool.call del primo mod. Quando un hook gestito blocca la chiamata, nessun hook tool.call di alcun mod viene eseguito per quella chiamata.
  • Hook PreToolUse da ogni altro file di impostazioni e da hooks/hooks.json dei plugin: vengono eseguiti dopo che l'ultimo mod ha chiamato next, come parte del comportamento proprio di Claude Code. Un mod che risponde a tool.call senza chiamare next ne impedisce l'esecuzione, e un mod che chiama next vede la loro decisione nel risultato che restituisce.

tool.check si attiva dopo che gli hook PreToolUse sono stati eseguiti. Per le regole di permesso e le decisioni degli hook PreToolUse che l'allow di un hook tool.check non può sostituire, consulta Estendere i permessi con gli hook.

Gestire un hook che fallisce

Un hook che fallisce non interrompe la sessione, e puoi decidere cosa succede al suo posto. Quando un hook senza un gestore .catch genera un'eccezione, va in timeout o restituisce un risultato con la forma sbagliata, ciò che succede dopo dipende dal fatto che abbia chiamato next:

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

Una riga indica il mod, l'evento e il motivo, ad esempio my-mod: tool.call hook skipped: threw Error: boom. Dove la leggi dipende dalla sessione, come elencato in Scoprire perché un mod non fa nulla. Un hook ui.render il cui albero non supera la validazione conta come un hook che ha restituito un risultato con la forma sbagliata.

Per fare in modo che un hook che blocca le chiamate fallisca in modo chiuso, aggiungi un gestore di errori .catch che risponda al suo posto. Qui, guard è la tua funzione hook, e il gestore verifica next.called per capire se guard aveva già chiamato next quando è fallito:

// on returns a registration, and .catch attaches a handler to that one hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // guard had already called next, so return what came back
  if (next.called) return next(e)
  // next.error.kind says why the handler was asked, such as 'throw' or 'timeout'
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Quando guard genera un'eccezione o va in timeout su una chiamata Bash, Claude Code chiama il gestore con lo stesso evento:

  • guard è fallito prima di chiamare next: il comando non viene eseguito, e Claude legge il testo di deny con il tipo alla fine
  • guard è fallito dopo aver chiamato next: il next(e) del gestore si risolve nel risultato prodotto dalla chiamata di guard senza eseguire di nuovo il comando, e Claude legge quel risultato

Il gestore ha un proprio limite di tempo più breve. Se il gestore stesso genera un'eccezione o va in timeout, Claude Code salta l'hook come se non avesse alcun gestore. Se guard non aveva chiamato next, il comando prosegue quindi come farebbe senza il mod.

La stessa forma del gestore si adatta a una protezione su prompt.submit o config.set. Quando next.called è false, restituisci il rifiuto che il riferimento degli eventi elenca per quell'evento: { drop: 'the reason' } per prompt.submit, { deny: 'the reason' } per config.set.

Su tool.check e plugin.register, un rifiuto restituito dopo che next è stato risolto rimane comunque valido, quindi restituiscilo senza verificare next.called:

Su tool.call, un deny restituito dopo che next è stato risolto nasconde il risultato a Claude.

Passaggi successivi