Permessi dell'Agent SDK: l'ordine in sei passi
- I sei passi che l'Agent SDK percorre per ogni chiamata a uno strumento, in ordine
- Quali passi possono solo negare, quali solo consentire e quali entrambe le cose — il modello mentale che rende ovvio il resto
- Perché uno strumento approvato presto non raggiunge mai la tua callback canUseTool, e cosa usare invece quando un controllo deve girare a ogni chiamata
- Le cinque configurazioni in cui allowedTools sembra un lucchetto e non lo è
- In cosa differiscono le regole deny quando nomini uno strumento rispetto a quando limiti un pattern al suo interno
- Cosa eredita un subagent — e l'unica modalità che rifiuta di ereditare
- L'interruttore di sicurezza che nessuna regola, modalità o hook può scavalcare
Quasi tutti configurano i permessi di un agente come configurerebbero un firewall: scrivono le regole e presumono che vinca la più specifica. Il Claude Agent SDK non funziona così. Percorre una pipeline fissa di sei passi per ogni chiamata a uno strumento, e ogni passo ha un potere diverso — alcuni possono solo bloccare, altri solo approvare, e solo due possono fare entrambe le cose.
Sbaglia l'ordine e distribuisci un agente che sembra blindato e non lo è. È la classe più comune di bug di harness, e quasi mai è un refuso in una regola.
I sei passi
- Il tuo hook
PreToolUsegira per primo, prima di ogni altro controllo. Il deny di un hook è definitivo — blocca la chiamata anche in modalitàbypassPermissions. Unallowdi un hook, invece, non è definitivo: i passi 2 e 3 girano comunque. - Provengono da
disallowedToolsesettings.json. Una corrispondenza blocca la chiamata in ogni modalità,bypassPermissionscompresa. - Provengono da
settings.json. Una corrispondenza non approva né blocca — instrada la chiamata fino al passo 6, la tua callback, anche in modalitàbypassPermissions. - La modalità attiva dice la sua qui.
bypassPermissionsapprova ciò che la raggiunge;acceptEditsapprova le operazioni sui file nell'ambito consentito;plancostringe gli strumenti di modifica file e di scrittura da shell a passare dalla tua callback a prescindere dalle regole allow. Le altre modalità lasciano passare. - Provengono da
allowedToolsesettings.json. Una corrispondenza approva la chiamata. Anche le chiamate che lo strumento approva da sé si risolvono qui, senza bisogno di regole — una lettura di file dentro le tue cartelle di lavoro, un comando Bash di sola lettura. - Tutto ciò che è ancora irrisolto raggiunge la tua callback per una decisione. In modalità
dontAskquesto passo viene saltato e la chiamata viene negata.
Il modello che dà senso a tutto
Dimentica la precedenza. Chiediti invece: cosa può fare questo passo?
| Passo | Può negare | Può approvare | Può instradare alla tua callback |
|---|---|---|---|
| 1. Hook | Sì — definitivo, sopravvive a bypassPermissions | Sì, ma i passi 2–3 girano comunque | — |
| 2. Regole deny | Sì — definitivo, sopravvive a bypassPermissions | No | — |
| 3. Regole ask | No | No | Sì — sopravvive a bypassPermissions |
| 4. Modalità di permesso | No | Sì (bypassPermissions, acceptEdits) | Sì (plan, per le scritture) |
| 5. Regole allow | No | Sì | No |
| 6. canUseTool | Sì | Sì | — |
Tre conseguenze discendono direttamente da quella tabella, e spiegano quasi ogni sorpresa:
- Deny batte tutto. Le regole deny e i rifiuti degli hook stanno a monte della modalità, quindi non esiste modalità che le sblocchi. È l'unico meccanismo su cui puoi contare per togliere capacità.
- Allow non batte niente. Le regole allow stanno al passo 5, sotto la modalità. Non possono limitare una modalità permissiva — si limitano a pre-approvare lavoro che altrimenti avrebbe richiesto conferma.
- La tua callback è l'ultima risorsa, non il cancello. Gira solo sulle chiamate che nulla più in alto ha risolto. Ogni controllo che metti al suo interno viene saltato per tutto ciò che è stato approvato prima.
Se scrivi controlli di sicurezza dentro canUseTool, li hai messi nel posto sbagliato. Una chiamata approvata da una regola allow, da acceptEdits o da bypassPermissions non raggiunge mai la callback — il tuo controllo non fallisce, semplicemente non gira. I controlli che devono valere per ogni chiamata vanno in un hook PreToolUse, che gira al passo 1 e il cui rifiuto sopravvive persino a bypassPermissions.
Cinque modi in cui allowedTools non è un lucchetto
allowedTools si legge come una allowlist. È una lista di pre-approvazione. Tutto ciò che lasci fuori è comunque disponibile a Claude; semplicemente ricade nella modalità e poi nella tua callback. Cinque trappole specifiche:
1. Non vincola bypassPermissions
È la più grossa. Gli strumenti non elencati non corrispondono ad alcuna regola allow, quindi ricadono al passo 4 — dove bypassPermissions li approva.
Sembra blindato. Approva Bash, Write ed Edit.
// WRONG — bypassPermissions approves everything unlisted
const options = {
allowedTools: ["Read"],
permissionMode: "bypassPermissions"
};Se ti serve bypassPermissions ma vuoi eliminare strumenti specifici, l'unica leva che funziona è disallowedTools — atterra al passo 2, a monte della modalità.
2. dontAsk è la modalità che rende esatta una allowlist
Per ottenere "questi strumenti e nient'altro", abbina la pre-approvazione a una modalità che nega invece di chiedere.
Un agente di sola lettura, per davvero
const options = {
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "dontAsk"
};Gli strumenti elencati girano. Tutto il resto che avrebbe richiesto conferma viene negato, e canUseTool non viene mai chiamata. Nota cosa gira comunque senza essere elencato: i comandi Bash di sola lettura, le letture di file dentro le tue cartelle di lavoro e strumenti come Agent che non chiedono prima di partire. Per togliere uno di questi del tutto dalla richiesta, aggiungi il suo nome semplice a disallowedTools.
3. Un nome semplice approva l'intero strumento
La copertura dipende dalla forma della voce, e la differenza è grande:
| Voce | Cosa pre-approva |
|---|---|
"Read" | Ogni chiamata Read (tranne le letture da percorsi di rete) |
"Bash(npm test *)" | Solo le chiamate Bash corrispondenti. Le altre chiamate Bash ricadono comunque nella tua callback |
L'ambito è tutto. "Bash" in una allow list è una postura di sicurezza diversa da "Bash(npm test *)", e il sistema di tipi non te lo dirà.
4. I glob non ancorati vengono ignorati in silenzio
Le regole allow accettano glob sui nomi degli strumenti solo dopo un prefisso letterale mcp__<server>__, e il segmento del server deve essere privo di glob.
| Voce | Effetto |
|---|---|
"mcp__puppeteer__*" | Funziona — ogni strumento del server puppeteer |
"mcp__github__get_*" | Funziona — gli strumenti get_ di quel server |
"*" o "mcp__*" | Ignorata. Non pre-approva nulla |
L'ultima riga fallisce nella direzione silenziosa e sicura — ottieni più richieste, non più permessi — ma se l'hai scritta aspettandoti un'approvazione generale, ci perderai un pomeriggio.
5. TypeScript ti avverte, su un canale che non stai ascoltando
Se passi una callback canUseTool in una configurazione in cui l'SDK sa che la callback verrà oscurata, emette un avviso di processo Node una tantum con il codice CLAUDE_SDK_CAN_USE_TOOL_SHADOWED. Lo innescano due configurazioni: permissionMode: "bypassPermissions" e ogni voce semplice di allowedTools come "Read".
Porta l'avviso di oscuramento nei tuoi log
process.on("warning", (w) => {
if (w.code === "CLAUDE_SDK_CAN_USE_TOOL_SHADOWED") {
console.error("[harness] canUseTool will be skipped:", w.message);
}
});Le voci con ambito come Bash(ls *) e la modalità acceptEdits non lo innescano, e le regole allow provenienti dai file di impostazioni sono invisibili al controllo — quindi un'esecuzione pulita non prova che la tua callback sia raggiungibile.
Regole deny: nominare uno strumento o limitarlo al suo interno
Le voci deny si comportano in modo diverso a seconda della forma, e la differenza riguarda ciò che Claude può vedere, non solo ciò che può fare.
| Voce | Effetto |
|---|---|
disallowedTools: ["Bash"] | La definizione dello strumento Bash viene rimossa dalla richiesta. Claude non vede mai lo strumento e non può tentarlo |
disallowedTools: ["Bash(rm *)"] | Bash resta disponibile. Le chiamate che corrispondono a rm * come scritto vengono negate in ogni modalità. Le altre chiamate Bash — compresa /bin/rm — ricadono |
disallowedTools: ["*"] | Ogni definizione di strumento viene rimossa. I glob sui nomi degli strumenti sono supportati nelle regole deny: "mcp__*" corrisponde a ogni strumento MCP di tutti i server |
Due cose da interiorizzare. Primo, la forma semplice è strettamente più forte: togliere lo strumento dal contesto batte il negare le sue chiamate, perché uno strumento che Claude non vede non genera tentativi falliti né nuovi tentativi lungo un altro percorso. Secondo, Bash(rm *) corrisponde alla stringa come scritta — il buco di /bin/rm non è un bug, è l'ambito documentato di una regola testuale. Tratta i rifiuti Bash con ambito come protezioni contro l'ovvio, mai come un confine di sicurezza.
Regole sui percorsi: quattro ancoraggi e una lettera morta
Le regole Read e Edit con ambito accettano un pattern di percorso, e due dettagli fregano tutti:
Edit(path)governa ogni strumento integrato che scrive file — compresiWriteeNotebookEdit. Una regolaWrite(path)non viene mai confrontata dai controlli di permesso sui file. Se hai scrittoWrite(//secrets/**), non hai scritto nulla.- Una doppia barra significa il filesystem.
Edit(//secrets/**)blocca le scritture ovunque sotto/secretssul disco. Una singola barra iniziale ancora invece alla sorgente della regola — per una regola passata tramiteallowedToolsodisallowedTools, è la cartella di lavoro della sessione, quindiEdit(/secrets/**)non protegge/secretssul disco.
Blocca le scritture su un vero percorso del filesystem
// // = absolute filesystem path. One slash anchors at the rule's source.
const options = {
disallowedTools: ["Edit(//etc/**)", "Edit(//secrets/**)", "Read(//secrets/**)"]
};Le modalità, e cosa compra davvero ciascuna
Due comportamenti da fissare bene perché in produzione mordono:
La modalità auto degrada in modo quasi silenzioso. Se passi con setModel() o applyFlagSettings() a un modello che auto non supporta, la sessione passa alla modalità default. Ricevi un messaggio system con sottotipo status e permissionMode: "default" — tipizzato come SDKStatusMessage in TypeScript — e da quel momento le chiamate raggiungono la tua callback. Tornare al modello precedente non ripristina auto; devi chiamare di nuovo setPermissionMode("auto"). (v2.1.299 o successive.)
I subagent ereditano, con un rifiuto. Un subagent gira nella modalità di permesso del genitore, a meno che tu imposti permissionMode sulla sua AgentDefinition e il genitore sia in default, dontAsk o plan. Anche in quel caso, Claude Code non applica mai un valore "bypassPermissions" preso da una AgentDefinition — un subagent gira in bypassPermissions solo quando lo fa la sessione genitore stessa (v2.1.267+). Questa eccezione esiste perché i subagent hanno spesso prompt di sistema diversi e un comportamento più lasco del tuo agente principale, quindi ereditare il bypass consegnerebbe pieno accesso autonomo al sistema alla parte meno supervisionata del tuo harness. Vedi Subagent.
L'interruttore di sicurezza che nulla scavalca
Le rimozioni con rm e rmdir che puntano a un percorso critico non vengono mai approvate da una regola allow né da un hook PreToolUse che restituisce allow. I percorsi critici sono la radice del filesystem, qualsiasi figlio diretto della radice (/usr, /etc, /data), la tua home directory, le radici dei drive Windows e le loro cartelle di primo livello, e la tua cartella di lavoro e le sue superiori. Una regola deny corrispondente blocca comunque il comando del tutto; il punto è che nessun percorso di approvazione può sbloccarlo.
Cosa succede invece dipende dalla modalità:
| Modalità | Esito per la rimozione di un percorso critico |
|---|---|
default, acceptEdits, bypassPermissions | Chiede approvazione |
auto | Chiede nel terminale, con un limite di tempo. Altrove — compresa ogni sessione dell'Agent SDK — la nega |
dontAsk | La nega |
La riga centrale è quella che conta per un harness: una sessione dell'Agent SDK in modalità auto nega le rimozioni di percorsi critici senza chiamare la tua callback, e dice a Claude di riferire cosa voleva cancellare. Se il tuo agente comincia all'improvviso a raccontare cancellazioni che non ha eseguito, è per questo — non per una regressione del modello.
Il controllo arriva anche dentro i costrutti in cui una variabile non impostata si espanderebbe in una cancellazione della radice: rm -rf "$DIR"/*, rm -rf "$1"/* senza niente che leghi $1, D=$(pwd); rm -rf "$D", e rimozioni annidate in subshell, sostituzioni di comando o script bash -c. La correzione documentata è proteggere l'espansione in modo che la shell dia errore se è vuota invece di cancellare la radice.
Fai passare una rimozione con glob il controllo dei percorsi critici
# Flagged: expands to a removal from / when DIR is unset
rm -rf "$DIR"/*
# Passes: :? makes the shell exit with an error instead
rm -rf "${DIR:?}"/*Altre cose che nessuna modalità approva automaticamente
Oltre alle rimozioni di percorsi critici, queste sfuggono sempre a una modalità permissiva — nella maggior parte delle modalità raggiungono la tua callback, e in dontAsk vengono negate perché quella modalità non chiede mai:
- Gli strumenti corrispondenti a una regola ask esplicita
AskUserQuestion, e gli strumenti MCP il cui server imposta_meta["anthropic/requiresUserInteraction"](v2.1.199+)- Gli strumenti dei connettori che la tua organizzazione ha impostato su
ask— la callback riceve il motivoYour organization requires approval for this tool - Le letture da percorsi di rete, che nessuna regola allow approva
Quindi "ho impostato bypassPermissions, perché mi viene ancora chiesto?" ha quattro risposte prima di arrivare a una segnalazione di bug. E nell'SDK TypeScript c'è una quinta leva: permissionPrompts: "none" (v2.1.259+) impedisce del tutto che la tua callback venga chiamata al passo 6 — un hook PermissionRequest ha comunque una possibilità, e se rinuncia a decidere, la chiamata viene negata.
Due ricette
Un cancello che gira davvero a ogni chiamata
Quando un controllo deve essere incondizionato — registro di audit, scanner di percorsi segreti, tetto di costo — mettilo al passo 1.
Un hook PreToolUse ha due modi per bloccare, entrambi disponibili per un hook di comando che puoi eseguire anche fuori dall'SDK: exit code 2, dove ciò che scrivi su stderr diventa il motivo che Claude vede, oppure JSON su stdout con un permissionDecision pari a "deny" più un permissionDecisionReason.
Hook PreToolUse: l'unico cancello incondizionato
# Runs before deny rules, ask rules, the mode, and allow rules.
# A deny here survives bypassPermissions.
# Wire it to hooks.PreToolUse in settings.json, or pass it in the SDK's hooks option.
jq -e '.tool_input | tostring | test("AKIA|BEGIN PRIVATE KEY|ghp_")' >/dev/null \
&& { echo "secret material in tool input" >&2; exit 2; } || exit 0La forma di callback in-process accettata dagli SDK TypeScript e Python restituisce gli stessi campi di decisione invece di un exit code. Conferma la forma del wrapper sul riferimento degli hook per la versione del tuo SDK prima di distribuire — la posizione di valutazione è la parte stabile, la firma del gestore un po' meno.
Stringere man mano che cresce la fiducia
La modalità di permesso non è una decisione una tantum. In una sessione in streaming puoi partire restrittivo e allentare dopo aver rivisto le prime mosse dell'agente, con set_permission_mode() in Python o setPermissionMode() in TypeScript. La nuova modalità si applica subito a tutte le richieste successive agli strumenti.
Parti da default, allenta dopo aver rivisto l'approccio
const q = query({
prompt: "Refactor the auth module",
options: {permissionMode: "default"}
});
// ...review the first turns, then:
await q.setPermissionMode("acceptEdits");La mossa inversa è più interessante per un'esecuzione lunga: stringi a dontAsk quando l'agente entra in una fase che non dovrebbe richiedere nuove capacità.
Da dove possono venire le regole
Le regole allow, deny e ask possono anche stare in modo dichiarativo in .claude/settings.json. L'SDK le legge quando la fonte di impostazioni project è abilitata, cosa che accade per le opzioni predefinite di query() — ma se imposti settingSources in modo esplicito, devi includere "project" o le regole dei file di impostazioni smettono di applicarsi in silenzio. È un classico incidente: qualcuno aggiunge settingSources: ["user"] per riproducibilità ed elimina in silenzio l'intera deny list del progetto. Vedi Impostazioni per la precedenza dei file, e ricorda che anche le regole allow dei file di impostazioni sono invisibili al controllo CLAUDE_SDK_CAN_USE_TOOL_SHADOWED.
Check yourself
0/3- Sei passi, ordine fisso: hook, regole deny, regole ask, modalità di permesso, regole allow, canUseTool.
- Deny è a monte della modalità e quindi assoluto. Allow è a valle e può solo pre-approvare.
- allowedTools è una lista di pre-approvazione, non una allowlist — abbinala a dontAsk per renderla esatta.
- I controlli in canUseTool non girano per le chiamate approvate automaticamente. I controlli incondizionati vanno in un hook PreToolUse.
- Edit(path) copre Write e NotebookEdit; una regola Write(path) non corrisponde a nulla. // ancora al filesystem, una singola / alla sorgente della regola.
- Un subagent eredita la modalità del genitore ma non eredita mai un valore bypassPermissions dalla propria definizione.
- Le rimozioni rm di percorsi critici sono un interruttore di sicurezza che nessuna regola allow o allow di hook può sbloccare — e una sessione dell'Agent SDK in modalità auto le nega senza chiamarti.
- Impostare settingSources senza "project" scarta in silenzio ogni regola di .claude/settings.json.
Prossimi passi
- Permessi e modalità di permesso — il lato interattivo dello stesso sistema, incluso perché
autoè diventata la modalità iniziale - Hook — il passo 1, l'unico cancello incondizionato della pipeline
- Rendere sicure le esecuzioni autonome — la checklist che le primitive di questa pagina implementano
- Modalità headless e Agent SDK — far partire il ciclo in primo luogo