Passa al contenuto principale

Permessi dell'Agent SDK: l'ordine in sei passi

Avanzato
What you'll learn
  • 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​

Guided walkthrough1 of 6
  1. Il tuo hook PreToolUse gira per primo, prima di ogni altro controllo. Il deny di un hook è definitivo — blocca la chiamata anche in modalità bypassPermissions. Un allow di un hook, invece, non è definitivo: i passi 2 e 3 girano comunque.

Il modello che dà senso a tutto​

Dimentica la precedenza. Chiediti invece: cosa può fare questo passo?

PassoPuò negarePuò approvarePuò instradare alla tua callback
1. HookSì — definitivo, sopravvive a bypassPermissionsSì, ma i passi 2–3 girano comunque—
2. Regole denySì — definitivo, sopravvive a bypassPermissionsNo—
3. Regole askNoNoSì — sopravvive a bypassPermissions
4. Modalità di permessoNoSì (bypassPermissions, acceptEdits)Sì (plan, per le scritture)
5. Regole allowNoSìNo
6. canUseToolSì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.
Watch out

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:

VoceCosa 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.

VoceEffetto
"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.

VoceEffetto
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 — compresi Write e NotebookEdit. Una regola Write(path) non viene mai confrontata dai controlli di permesso sui file. Se hai scritto Write(//secrets/**), non hai scritto nulla.
  • Una doppia barra significa il filesystem. Edit(//secrets/**) blocca le scritture ovunque sotto /secrets sul disco. Una singola barra iniziale ancora invece alla sorgente della regola — per una regola passata tramite allowedTools o disallowedTools, è la cartella di lavoro della sessione, quindi Edit(/secrets/**) non protegge /secrets sul 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​

Premi Invio o Spazio per girare la carta. Usa le frecce sinistra e destra per spostarti tra le carte.Termine mostrato.
1 / 6

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, bypassPermissionsChiede approvazione
autoChiede nel terminale, con un limite di tempo. Altrove — compresa ogni sessione dell'Agent SDK — la nega
dontAskLa 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 motivo Your 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 0

La 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
  1. Imposti allowedTools: ["Read"] e permissionMode: "bypassPermissions". L'agente può eseguire Bash?
  2. Un controllo di sicurezza deve girare su ogni singola chiamata a uno strumento, senza eccezioni. Dove va messo?
  3. Quale voce deny impedisce a Claude persino di vedere lo strumento Bash?
Key takeaways
  • 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​