Una macchina sola, molte superfici
Claude Code, l’app desktop, claude.ai, l’API e gli SDK sembrano prodotti diversi e in buona parte non lo sono. La documentazione lo dice esplicitamente: «The interface determines how you see and interact with Claude, but the underlying agentic loop is identical.» Quello che cambia davvero, e che devi progettare, è dove vive lo stato e dove vive l’istruzione.
Sullo stato la differenza è netta. La Messages API è senza stato: «The Messages API is stateless, which means that you always send the full conversational history to the API.» La cronologia è una struttura dati tua, che ricostruisci a ogni chiamata. Claude Code invece persiste la conversazione su disco in JSONL sotto ~/.claude/projects/, ma ogni nuova sessione parte con una context window vuota: quello che sopravvive fra sessioni non è la chat, sono i file.
Sull’istruzione conta la gerarchia. Nell’API il canale autorevole è il campo system. In Claude Code c’è CLAUDE.md, e qui va corretta un’assunzione comune: non è un system prompt e non è configurazione applicata. Viene consegnato come messaggio utente dopo il system prompt, quindi Claude lo legge e cerca di seguirlo senza garanzia di conformità. Se una cosa deve accadere sempre, non è materiale da CLAUDE.md: è un hook PreToolUse o una regola in permissions.deny.
Confini del contenuto
Sui modelli recenti puoi inserire un messaggio con ruolo system anche a metà conversazione, dopo un turno utente. Ha la stessa autorità del campo system di primo livello, ma essendo in coda alla cronologia non invalida il prefisso già in cache: è il modo giusto per attivare una regola che diventa rilevante solo dopo. Non può però essere il primo elemento di messages.
Il confine più importante è però un altro: tutto ciò che entra da fuori — una pagina web, un file caricato, il risultato di un tool — è dato, non istruzione. Anthropic descrive una difesa a strati, con addestramento specifico, classificatori che scansionano il contenuto non fidato in ingresso e red teaming umano, e allo stesso tempo rifiuta di dichiarare il problema chiuso: «No browser agent is immune to prompt injection, and we share these findings to demonstrate progress, not to claim the problem is solved.» Progettare di conseguenza significa non lasciare al modello l’unico controllo: delimita il contenuto non fidato, non permettere che sia lui a determinare quale tool viene chiamato, e metti ogni azione irreversibile dietro un permesso che decide la tua applicazione.
Schemi: due meccanismi, due problemi
Uscite strutturate e tool strict risolvono cose diverse. output_config.format con type: "json_schema" vincola la forma della risposta, cioè quello che Claude dice; strict: true su una definizione di tool valida gli argomenti, cioè come Claude chiama la tua funzione. Si usano insieme.
Il modo tipico di sbagliare è riciclare uno schema di dominio esistente. Il sottoinsieme di JSON Schema accettato è ristretto: passano enum, const, $ref interni, required e additionalProperties, che deve valere false. Restano fuori gli schemi ricorsivi, i $ref esterni, i vincoli numerici come minimum e maximum, quelli di lunghezza come minLength, e minItems con valori diversi da 0 e 1. Se il tuo schema li usa, ti serve una versione ridotta per il modello e la validazione fine resta a valle.
Per i tool il nome deve rispettare un pattern preciso:
^[a-zA-Z0-9_-]{1,64}$
Sul contenuto della definizione la documentazione è insolitamente diretta sulle descrizioni: «This is by far the most important factor in tool performance», con l’indicazione di puntare ad almeno tre o quattro frasi per tool, spiegando anche quando il tool non va usato. Le altre due regole che spostano il risultato: consolidare operazioni correlate in un tool solo con un parametro action, invece di moltiplicare tool quasi identici, e usare un namespace nel nome quando i tool coprono più servizi.
Igiene di sessione
In Claude Code i tre comandi che contano sono /clear, che riparte con contesto vuoto lasciando la conversazione precedente recuperabile con /resume; /compact, che sostituisce la cronologia con un riassunto e accetta istruzioni; e /context, che mostra cosa occupa spazio. /branch copia la conversazione fino a quel punto e ti sposta nella copia, lasciando intatta l’originale: è il modo per provare una seconda strada senza perdere la prima.
Il fatto da ricordare sulla compattazione è asimmetrico: il CLAUDE.md di progetto viene riletto da disco e re-iniettato dopo il compact, mentre un’istruzione data solo a voce nella conversazione può sparire. Se una regola ti serve fino alla fine del lavoro, scrivila su file.
Lato API l’igiene ha una forma diversa: la sessione è la lista di messaggi che mandi tu, e i blocchi thinking dell’ultimo turno assistant vanno rimandati indietro esattamente come li hai ricevuti, inclusi quelli con campo vuoto, altrimenti la richiesta torna 400.
Plugin
Skill, agenti e hook possono stare in .claude/ come configurazione standalone oppure dentro un plugin. La differenza pratica è la condivisione e il namespace: le skill di un plugin si invocano come /plugin-name:skill-name, quindi due plugin con una skill omonima non collidono. In sviluppo si carica con claude --plugin-dir ./my-plugin e si ricarica con /reload-plugins; per il team si abilita a livello di repository con enabledPlugins.
Una trappola nella migrazione da .claude/ a plugin: gli agenti definiti in .claude/agents/ di progetto o utente hanno la precedenza su un agente di plugin con lo stesso nome, quindi finché non rimuovi gli originali la versione del plugin non entra mai in gioco. Le skill invece convivono, proprio perché sono namespacate.