SpyBara
Go Premium

self-hosted-environments-configuration.md 2026-11-11 22:59 UTC to 2026-11-12 22:01 UTC

No changes for this document in this history range.

2026
Tue 3 23:58 Wed 4 23:59 Fri 6 23:01 Mon 9 23:02 Tue 10 21:57 Wed 11 22:59

Personalizzare le sessioni negli ambienti self-hosted

Personalizzare le sessioni degli ambienti self-hosted con script wrapper per credenziali per sessione, hook del ciclo di vita e spawning di runner su richiesta.

[!NOTE]

Gli ambienti self-hosted sono in beta pubblica sui piani Team ed Enterprise; un Owner li abilita attivando Allow self-hosted environments nella pagina di amministrazione Cloud environments. Questa pagina presuppone un runner funzionante; consulta la guida rapida per la configurazione e Deploy to production per le ricette della flotta.

Un ambiente self-hosted esegue le sessioni cloud di Claude Code sulla tua infrastruttura, eseguite da un processo runner che distribuisci. Senza configurazione, quel runner clona il repository della sessione, avvia Claude Code e pulisce. Questa pagina è per l'ingegnere della piattaforma che gestisce i runner: copre i punti di estensione per quando questi valori predefiniti non si adattano, dal provisioning delle credenziali per sessione alla sostituzione completa del checkout. I wrapper e gli hook vengono eseguiti come file eseguibili sull'host del runner, che è Linux o macOS, e gli esempi su questa pagina presuppongono una shell POSIX.

Alcune variabili d'ambiente degli hook su questa pagina utilizzano ancora pool, come CLAUDE_RUNNER_POOL_ID; i nomi dei flag CLI e delle variabili d'ambiente utilizzano environment, come --environment-secret-file.

Script wrapper

Usa uno script wrapper quando ogni sessione ha bisogno di una configurazione che il runner non può fare da solo:

  • Provisioning di credenziali di breve durata limitate al creatore della sessione
  • Esportazione di segreti specifici dell'ambiente
  • Preparazione di toolchain di linguaggio
  • Applicazione di limiti di risorse attorno al processo figlio

Il runner avvia il tuo wrapper al posto del binario Claude Code ogni volta che avvia una sessione, anche quando una sessione viene ripresa o rimessa in coda. Termina il wrapper con exec in $CLAUDE_RUNNER_CLAUDE_BIN, il binario del runner stesso, in modo che i segnali e i codici di uscita si propaghino correttamente.

Punta --exec-path, o SELF_HOSTED_RUNNER_EXEC_PATH, al wrapper quando avvii il runner, sostituendo <writable-dir> come descritto nella guida rapida:

claude self-hosted-runner --environment-secret-file /etc/claude/environment-secret --base-dir '<writable-dir>' --exec-path /etc/claude/session-wrapper.sh

Il runner imposta quanto segue nell'ambiente del wrapper:

Variabile Descrizione
CLAUDE_CODE_SESSION_ACCESS_TOKEN Il JWT della sessione, con prefisso sk-ant-cc-. Il suo claim act identifica il creatore della sessione, con l'email del creatore quando la superficie di creazione l'ha registrata. Il valore è il token al momento dello spawn; gli aggiornamenti arrivano sullo stdin del figlio, quindi un wrapper vede solo il valore iniziale. Consulta Verify session identity.
CCR_SESSION_ACCOUNT_EMAIL L'email del creatore della sessione, pre-estratta dal runner dal claim act.email del token senza verifica della firma. Adatta per l'etichettatura, come i trailer dei commit. Quando l'email controlla il rilascio delle credenziali, verifica il token e leggi il claim da esso. Consulta Provision credentials scoped to the session creator. Non impostata quando il token non contiene un'email del creatore, ad esempio nelle sessioni create dall'identità di servizio della tua organizzazione. Trattala come informazione personale identificabile.
CLAUDE_RUNNER_CLIENT_PLATFORM La superficie client che ha creato la sessione, come web_claude_ai, desktop_app, ios, claude_code_cli o scheduled_trigger. Anthropic registra il valore una volta alla creazione della sessione, quindi il wrapper e ogni hook del ciclo di vita vedono lo stesso valore. Usala solo per l'analisi dell'adozione e l'etichettatura, non come segnale di autorizzazione. Non impostata quando la sessione non ha una superficie registrata o riconosciuta. Richiede Claude Code v2.1.229 o successivo.
CLAUDE_RUNNER_CLAUDE_BIN Percorso assoluto al binario Claude Code del runner stesso. Termina il wrapper con exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" per passare il controllo al binario bloccato senza hardcodificare un percorso di installazione.
CLAUDE_CODE_REMOTE_SESSION_ID ID sessione nel formato taggato cse_.... Questa è la stessa sessione che gli hook del ciclo di vita vedono come CLAUDE_RUNNER_SESSION_ID nel formato session_...; le variabili UUID corrispondono in entrambi, e sostituire il prefisso cse_ con session_ produce l'ID mostrato nell'URL della sessione.
CLAUDE_CODE_REMOTE_SESSION_UUID Lo stesso ID sessione nel formato UUID canonico, per i sistemi che si basano su UUID.
CLAUDE_CODE_REMOTE_SLACK_THREAD_URL Per una sessione Claude Tag che appartiene a un thread Slack, il link a quel thread. Non impostata per le altre sessioni, e può non essere impostata anche per una sessione di thread.
CLAUDE_CODE_REMOTE_SLACK_THREAD_TS Per una sessione Claude Tag che appartiene a un thread Slack, il timestamp Slack di quel thread, come 1700000000.000100. Può non essere impostata, e può essere impostata quando CLAUDE_CODE_REMOTE_SLACK_THREAD_URL non lo è, quindi controlla ciascuna variabile separatamente.
CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID Per una sessione Claude Tag, l'ID del canale Slack o del messaggio diretto che la sessione serve o in cui si trova il suo thread, come C0123ABCDE. Un timestamp Slack identifica un messaggio all'interno di un solo canale, quindi registra questo ID insieme a CLAUDE_CODE_REMOTE_SLACK_THREAD_TS. Non impostata per le altre sessioni, e può non essere impostata anche per una sessione Claude Tag.
CLAUDE_SESSION_INGRESS_TOKEN_FILE Percorso assoluto a un file per sessione che contiene il JWT della sessione corrente, mantenuto aggiornato negli aggiornamenti dei token. I sottoprocessi della shell lo leggono per il loro header Authorization quando scaricano gli allegati che l'utente ha aggiunto alla sessione. exec preserva la variabile automaticamente; un wrapper che ricostruisce l'ambiente del figlio deve riportare la variabile, altrimenti i download degli allegati smettono di funzionare silenziosamente.
CLAUDE_CONFIG_DIR Directory di configurazione Claude per sessione, scritta all'inizio della sessione dallo snapshot della configurazione dell'host del runner che il runner acquisisce all'avvio; consulta Permissions and tool approval. Le scritture qui sono isolate a questa sessione. La directory rimane sotto <base-dir>/_sessions/ dopo la fine della sessione a meno che tu non avvii il runner con --remove-session-state; consulta Reuse a pre-warmed checkout.
CLAUDE_CODE_REMOTE_MCP_CONFIG Percorso a un file in CLAUDE_CONFIG_DIR che contiene i server MCP forniti dal piano di controllo. Se il tuo wrapper ricostruisce l'ambiente del figlio, riporta invariate questa variabile, CLAUDE_CODE_REMOTE e CLAUDE_CODE_ENVIRONMENT_KIND; il runner le imposta tutte e tre. Se CLAUDE_CODE_REMOTE arriva ma questa variabile o CLAUDE_CODE_ENVIRONMENT_KIND no, il figlio arresta i server stdio, HTTP e SSE che il tuo wrapper aggiunge con --mcp-config e che un aggiornamento dei server forniti omette. Richiede Claude Code v2.1.299 o successivo.
ANTHROPIC_BASE_URL L'URL di base dell'API che il figlio utilizzerà, fornito dal piano di controllo per sessione e normalmente https://api.anthropic.com. Non sovrascriverlo: la credenziale di inferenza della sessione è un token OAuth emesso da Anthropic che altri provider non accettano.
CLAUDE_CODE_OAUTH_TOKEN Il token di accesso OAuth di breve durata che il figlio utilizza per l'inferenza del modello, con una durata di circa 30 minuti. Il runner lo riemette prima della scadenza e fornisce la rotazione sullo stdin del figlio, quindi un wrapper che non mantiene stdin collegato vede solo il valore iniziale. Non fare affidamento sull'allowlist IP della tua organizzazione per limitare l'uso di questo token: trattalo come una credenziale bearer che rimane utilizzabile per circa 30 minuti se trapela, e non registrarlo nei log, non scriverlo su disco né inoltrarlo al di fuori del container della sessione.
CLAUDE_RUNNER_ALIVE_EVERY_MS Il runner imposta la frequenza, in millisecondi, con cui il figlio segnala di essere attivo quando un turno inizia prima che il figlio si inizializzi. Se il tuo wrapper ricostruisce l'ambiente del figlio, riporta questa variabile, altrimenti il runner può rilasciare la sessione durante quel turno e registrare nel log no child output for <n> — releasing. Richiede v2.1.309 o successivo sul runner e sul figlio.

Il wrapper eredita anche il resto dell'ambiente gestito del figlio, incluse tutte le variabili d'ambiente fornite dal server. exec le propaga tutte automaticamente; se il tuo wrapper avvia il figlio in un altro modo, inoltra l'ambiente completo.

Puoi leggere le variabili CLAUDE_CODE_REMOTE_SLACK_* nel tuo wrapper o nell'hook command, e in ciò che la sessione esegue, come i comandi della shell, gli hook di git e gli hook di Claude Code. Gli hook checkout, post-session e spawn-runner non le ricevono.

Assegnare un valore predefinito alle variabili che possono non essere impostate

Se il tuo script usa set -u, espandi CCR_SESSION_ACCOUNT_EMAIL, CLAUDE_RUNNER_CLIENT_PLATFORM, CLAUDE_CODE_REMOTE_SLACK_THREAD_URL, CLAUDE_CODE_REMOTE_SLACK_THREAD_TS e CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID con un valore predefinito, come "${CCR_SESSION_ACCOUNT_EMAIL:-}". Ciascuna può non essere impostata, e con set -u Bash si arresta con unbound variable quando ne espande una non impostata.

Mettere tra virgolette gli argomenti e le variabili che il tuo script espande

Metti tra virgolette ogni argomento e ogni variabile che il runner passa al tuo wrapper o all'hook command. Alcuni valori contengono caratteri che una shell interpreta, perché contengono JSON, frasi intere o una regola di permesso come Bash(npm run test *).

  • Argomenti: passali come "$@"
  • Variabili: metti ciascuna tra virgolette, come in "${CLAUDE_CODE_REMOTE_SLACK_THREAD_URL:-}". Il link del thread Slack, ad esempio, può contenere ? e &.
  • Stringhe di eval e sh -c: non sostituire un valore in una stringa eseguita da eval o sh -c, nemmeno tra virgolette. Fai invece in modo che quella stringa faccia riferimento alla variabile.

Mantenere stdin e il descrittore di file 3 collegati

Lo stdin del figlio è il canale di controllo del runner. Le rotazioni dei token e i segnali di fine sessione arrivano su di esso. Il runner apre anche una pipe sul descrittore di file 3 e legge da essa i segnali di attività del figlio per gestire i timeout di inattività e di avvio. Un semplice exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" preserva entrambi automaticamente.

Se il tuo wrapper mette in background il figlio con un semplice &, interrompe lo stdin del figlio. La sessione sembra sana fino a quando non scade la durata di circa 30 minuti del token OAuth iniziale, dopodiché ogni chiamata API che usa il token fallisce con 401 authentication_error. Se il tuo wrapper deve mettere in background il figlio, ad esempio per mantenere attivo un trap di teardown, salva stdin sul descrittore di file 9 e ricollegalo esplicitamente:

exec 9<&0   # the runner uses 3, and from v2.1.309 reserves 4 and 5
"$CLAUDE_RUNNER_CLAUDE_BIN" "$@" <&9 9<&- &
CHILD=$!
trap 'teardown' EXIT
wait "$CHILD"

Puoi reindirizzare lo stdout del figlio. Mantieni il descrittore di file 3 e lo stderr collegati al runner:

  • Descrittore di file 3: trasporta i segnali di attività del figlio al runner. Non chiuderlo né riutilizzarlo nel wrapper.
  • Descrittori di file 4 e 5: i runner su Claude Code v2.1.309 o successivo li riservano al figlio. Non chiuderli né riutilizzarli nel wrapper. Se il tuo wrapper usa uno dei due, ad esempio per salvare stdin, sposta quell'uso su un descrittore da 6 a 9, che anche le versioni precedenti lasciano liberi.
  • stderr: quando il wrapper o il figlio esce con codice diverso da zero, il runner pubblica le ultime righe dello stderr nella sessione e le stampa nel proprio log. L'utente della sessione vede quelle righe, quindi non stampare segreti sullo stderr e rimuovi set -x prima di distribuire il wrapper. Se reindirizzi lo stderr, le sessioni vengono comunque eseguite, ma il runner segnala un errore con il solo codice di uscita.

Passare i flag del prompt di sistema

Il prompt di sistema e il prompt di sistema aggiuntivo che il piano di controllo di Anthropic invia per una sessione raggiungono il tuo wrapper come percorsi di file, non come testo inline. Il runner scrive ogni prompt in un file nella directory di configurazione della sessione, CLAUDE_CONFIG_DIR, e ne passa il percorso negli argomenti che il tuo wrapper riceve, come --system-prompt-file <path> o --append-system-prompt-file <path>.

I runner su Claude Code v2.1.281 o successivo forniscono i prompt come file. Prima della v2.1.281, il runner li passava come --system-prompt <text> e --append-system-prompt <text>.

Nel tuo script wrapper o hook command, gestisci questi flag come segue:

  • Passali così come sono: termina il wrapper con exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@", che inoltra i flag dei file insieme a ogni altro argomento. Non eliminarli né riscriverli. Se una sessione perde un flag del file di prompt, viene eseguita senza le istruzioni che il piano di controllo ha inviato per essa.
  • Su un runner alla v2.1.281 o successiva, un flag di file che aggiungi sostituisce quello del server, non vi si aggiunge mai: ogni flag del file di prompt accetta un solo valore e Claude Code mantiene l'ultima occorrenza, quindi se aggiungi --append-system-prompt-file <path> dopo "$@", il contenuto del tuo file sostituisce le istruzioni aggiuntive del server. Per aggiungere istruzioni oltre a quelle del server, inseriscile nel CLAUDE.md dell'immagine del runner, che il runner inserisce nella configurazione a livello utente di ogni sessione.

Correggere Argument list too long all'avvio della sessione

Su Linux, se unisci il prompt del piano di controllo e il tuo testo in un unico argomento --append-system-prompt, il runner registra una riga come questa quando l'argomento raggiunge 128 KB:

Session <id> finished: failed — reason: child exited 126: /opt/wrapper.sh: line 9: /usr/local/bin/claude: Argument list too long

Linux limita ogni singolo argomento a 32 pagine di memoria, ovvero 128 KB con le consuete pagine da 4 KB. Il valore restituito da getconf ARG_MAX, spesso 2 MB, è un limite separato su tutti gli argomenti e l'ambiente insieme.

Su Claude Code v2.1.283 o successivo, lascia i flag dei file in "$@" e passa solo il tuo testo come --append-system-prompt. Claude Code li usa entrambi, prima il prompt del piano di controllo e poi il tuo testo:

exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --append-system-prompt "$(cat /opt/team-instructions.md)"

Su Linux, se il tuo testo raggiunge 128 KB, spostalo nel CLAUDE.md dell'immagine del runner, perché è comunque un unico argomento.

Sulla v2.1.281 o v2.1.282, quel comando esce con Cannot use both --append-system-prompt and --append-system-prompt-file. Sulle versioni precedenti, il tuo testo sostituisce il prompt del piano di controllo senza alcun errore. Su ognuna di esse, aggiorna prima, oppure sposta il tuo testo in CLAUDE.md.

Provisioning di credenziali limitate al creatore della sessione

Usa il sottocomando decode-token per leggere i claim dal JWT della sessione. Legge il token da un argomento, da CLAUDE_CODE_SESSION_ACCESS_TOKEN o da stdin, in quest'ordine; consulta Verify the token inside the session per sapere cosa controlla. L'esempio seguente decodifica l'identità del creatore, la scambia con credenziali AWS di breve durata ed esegue exec in Claude Code:

#!/bin/bash
# Key on the stable Anthropic user ID and require a human creator.
CREATOR_SUB=$("$CLAUDE_RUNNER_CLAUDE_BIN" self-hosted-runner decode-token \
  | jq -re '.act.sub // "" | select(startswith("user:"))') \
  || { echo "decode-token: verification failed or no human creator" >&2; exit 1; }

creds=$(your-sts-helper assume-role --subject "$CREATOR_SUB") \
  || { echo "credential exchange failed" >&2; exit 1; }
eval "$creds"

exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@"

Usa jq -re anziché jq -r quando il claim estratto controlla una decisione di autenticazione, in modo che un claim assente esca con codice diverso da zero invece di passare a valle la stringa letterale null. Le sessioni create da un'identità di servizio dell'organizzazione, come le sessioni di bot e agenti, portano un soggetto agent: anziché user:, quindi questo esempio le rifiuta; se il tuo ambiente serve quelle sessioni, decidi esplicitamente se il wrapper debba ricorrere a una credenziale predefinita per esse invece di uscire. Quando il tuo scambio di credenziali ha invece bisogno dell'email, leggi .act.email e gestiscine l'assenza: il token la contiene solo quando la superficie di creazione l'ha registrata, e una sessione inviata da CLI può esserne priva. Per il riferimento completo dei claim e la verifica da servizi al di fuori del runner, consulta Verify session identity.

Hook del ciclo di vita

Gli hook del ciclo di vita sostituiscono le fasi della pipeline per sessione del runner con i propri script. Puntare il runner a una directory di hook con --hooks-dir <path>, o SELF_HOSTED_RUNNER_HOOKS_DIR. Il runner cerca file eseguibili con nomi ben noti; qualsiasi hook che non è presente ricade nel comportamento integrato, quindi si scrivono solo quelli di cui si ha bisogno. Gli hook vengono eseguiti con i privilegi del runner stesso, e i figli della sessione condividono quel UID, quindi montare la directory degli hook in sola lettura, o cuocerla nell'immagine, in modo che il codice della sessione non possa modificarla; consultare la sezione di hardening.

Questi hook sono distinti dagli hook di Claude Code, che vengono eseguiti all'interno della sessione; gli hook del ciclo di vita vengono eseguiti sul runner, attorno alla sessione.

checkout

Viene eseguito per ogni repository ogni volta che il runner avvia una sessione, anche quando la sessione riprende o viene rimessa in coda, al posto del clone e del fetch integrati del runner. Usa l'hook per clonare da un mirror read-through che raggiungi tramite HTTPS o SSH, popolare un albero di lavoro da un archivio o applicare l'autenticazione git per sessione.

Il runner imposta queste variabili, e può impostare altre variabili CLAUDE_RUNNER_ che la tabella non elenca:

Variabile Descrizione
CLAUDE_RUNNER_REPO_URL URL del repository da clonare, dopo che --git-host-rewrite e --git-ssh-rewrite sono stati applicati
CLAUDE_RUNNER_REPO_REF Revisione di cui eseguire il checkout, come richiesta dalla sessione: un branch, un tag, un commit SHA o un nome di riferimento completo come refs/heads/<branch> o refs/pull/<number>/head. Vuoto indica il branch predefinito del repository.
CLAUDE_RUNNER_CHECKOUT_PATH Percorso assoluto dove l'albero di lavoro deve essere lasciato. Consulta Nomi delle cartelle di checkout.
CLAUDE_RUNNER_SESSION_ID ID sessione nel modulo taggato session_..., per la registrazione e la correlazione
CLAUDE_RUNNER_SESSION_UUID Lo stesso ID sessione nel modulo UUID canonico
CLAUDE_RUNNER_API_BASE_URL URL di base dell'API Anthropic per le chiamate limitate alla sessione
CLAUDE_RUNNER_CLIENT_PLATFORM La superficie client che ha creato la sessione, come web_claude_ai, desktop_app o ios. Non impostata quando la sessione non ha una superficie registrata o riconosciuta, quindi fai riferimento a essa come "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" con set -u. Richiede Claude Code v2.1.229 o successivo.
CLAUDE_CODE_SESSION_ACCESS_TOKEN Il token di accesso della sessione, per le chiamate API limitate alla sessione
GIT_CONFIG_COUNT, GIT_CONFIG_KEY_n, GIT_CONFIG_VALUE_n Impostazioni git che il runner fissa per il git eseguito dal tuo hook. Configurazione git all'interno degli hook del ciclo di vita le descrive. Richiede Claude Code v2.1.280 o successivo.

Lo script deve lasciare in CLAUDE_RUNNER_CHECKOUT_PATH un albero di lavoro con il checkout della revisione richiesta. Un HEAD staccato va bene, perché il runner crea sopra di esso il branch di lavoro della sessione.

Dopo che il tuo hook termina, il runner verifica che CLAUDE_RUNNER_CHECKOUT_PATH contenga un .git. Se il tuo hook materializza una sorgente non git come Perforce o un tarball scompattato, imposta CLAUDE_RUNNER_SKIP_GIT_VERIFY=1 nell'ambiente del runner per saltare quel controllo. I flussi basati su git come la creazione del branch di lavoro e il push dei risultati richiedono un checkout git, quindi esporta i risultati da alberi non git con un hook post-session.

Nomi delle cartelle di checkout

CLAUDE_RUNNER_CHECKOUT_PATH termina con una cartella chiamata <repo-owner>-<repo>, come acme-api. Due repository in una sessione non possono condividere una cartella, anche quando i loro nomi differiscono solo per maiuscole e minuscole. Ad esempio, se una sessione elenca acme/api e poi Acme/API, il primo ottiene acme-api e il secondo ottiene Acme-API-2.

Ottenere le credenziali git nell'hook

Il runner non passa una credenziale git all'hook. Nemmeno il sottocomando decode-token è disponibile qui, perché CLAUDE_RUNNER_CLAUDE_BIN non è impostato nell'ambiente dell'hook di checkout. Genera invece una credenziale di clone per sessione a partire dall'identità della sessione, oppure ricorri all'autenticazione git dell'host stesso:

  • Credenziale di clone per sessione: verifica CLAUDE_CODE_SESSION_ACCESS_TOKEN con una libreria JWT standard rispetto all'endpoint JWKS sotto CLAUDE_RUNNER_API_BASE_URL, come descritto in Verify the token from your service. Poi fai in modo che il tuo servizio di credenziali emetta una credenziale di clone di breve durata per l'identità nel claim act del token. Associa quella credenziale a act.sub e non richiedere act.email.
  • Autenticazione git dell'host: usa qualsiasi autenticazione git di cui l'host dispone già, come un agente SSH, un credential helper o .netrc.

Quando l'hook fallisce

L'hook fallisce quando esce con un codice diverso da zero, o esce con 0 senza lasciare un checkout utilizzabile:

  • Un repository a cui la sessione spinge i risultati: il runner fa fallire la sessione e, in caso di uscita diversa da zero, mostra all'utente la coda dello stderr dello script. Scrivi il motivo nello stderr come un'unica riga breve, scrivilo per ultimo e poi esci con un codice diverso da zero. Non stampare segreti nello stderr e rimuovi set -x prima di distribuire l'hook.

    Se il messaggio couldn't find remote ref di git compare all'incirca negli ultimi 4 KB dello stderr dell'hook, il runner segnala invece un branch mancante. Se quel branch esiste, consulta Una sessione fallisce con couldn't be found in the remote repository o couldn't find remote ref.

  • Un repository che la sessione legge soltanto, come un repository aggiunto a una sessione in esecuzione: il runner lo salta e continua con i repository rimanenti. Il runner pubblica un passaggio Skipped nella sessione e rimuove ciò che l'hook ha lasciato nel percorso di checkout.

    Quando il passaggio Skipped menziona l'hook di checkout, trovi il dettaglio del fallimento nella riga [runner:warn] checkout hook failed del log del runner. Quando il passaggio indica che un ref di origine no longer exists on remote, consulta Una sessione fallisce con couldn't be found in the remote repository o couldn't find remote ref. Se saltarlo lascia la sessione senza alcun repository, il runner fa comunque fallire la sessione.

Quando l'hook ha successo, il runner rimuove il percorso di checkout dopo la fine della sessione.

post-session

Viene eseguito dopo che il figlio Claude Code è uscito e prima che il runner smantelli il workspace. Questo hook è la tua unica possibilità di salvare il lavoro non committato, perché i cambiamenti tracciati non committati non sopravvivono su nessuno dei due percorsi:

  • A --capacity superiore a uno: il runner elimina i worktree per sessione subito dopo il ritorno dell'hook
  • A --capacity 1: il clone canonico riutilizzato viene hard-reset quando la sessione successiva inizia

Gli usi tipici sono il push di un branch snapshot di cambiamenti non committati, l'archiviazione di log o l'emissione di un evento di fine sessione ai tuoi sistemi.

L'hook si attiva ad ogni fine sessione dove un processo figlio è stato generato, qualunque sia la causa; i valori CLAUDE_RUNNER_EXIT_REASON di seguito enumerano i casi. Non può attivarsi quando il runner termina bruscamente, come una preemption VM o una perdita di potenza; se hai bisogno di garanzie contro la terminazione brusca, fai uno snapshot periodicamente dall'interno della sessione con un hook Claude Code PostToolUse invece. Il runner imposta:

Variabile Descrizione
CLAUDE_RUNNER_SESSION_ID ID sessione nel modulo taggato session_...
CLAUDE_RUNNER_SESSION_UUID Lo stesso ID sessione nel modulo UUID canonico
CLAUDE_RUNNER_EXIT_REASON Come la sessione è terminata; consultare i valori sotto la tabella
CLAUDE_RUNNER_WORKSPACE_PATHS Percorsi assoluti separati da due punti degli alberi di lavoro della sessione. Vuoto per sessioni senza repository.
CLAUDE_RUNNER_DEBUG_LOG_PATH Percorso al log di debug della sessione, ancora su disco mentre l'hook viene eseguito
CLAUDE_RUNNER_API_BASE_URL URL di base dell'API Anthropic per le chiamate limitate alla sessione
CLAUDE_RUNNER_CLIENT_PLATFORM La superficie client che ha creato la sessione, come web_claude_ai, desktop_app o ios. Non impostata quando la sessione non ha una superficie registrata o riconosciuta, quindi fai riferimento a essa come "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" con set -u. Richiede Claude Code v2.1.229 o successivo.
CLAUDE_CODE_SESSION_ACCESS_TOKEN Il token di accesso della sessione, per le chiamate API limitate alla sessione
GIT_CONFIG_COUNT, GIT_CONFIG_KEY_n, GIT_CONFIG_VALUE_n Impostazioni git che il runner fissa per il git eseguito dal tuo hook. Configurazione git all'interno degli hook del ciclo di vita le descrive. Richiede Claude Code v2.1.280 o successivo.

CLAUDE_RUNNER_EXIT_REASON assume uno di quattro valori:

  • completed: la sessione è terminata in modo pulito. Il processo Claude Code è uscito normalmente, oppure è uscito da solo dopo che la sessione è stata archiviata o eliminata.
  • failed: il processo Claude Code è andato in crash, oppure la configurazione è fallita dopo l'avvio.
  • interrupted: il runner ha interrotto la sessione, in uno di questi casi:
    • Il runner ha rilasciato la sessione per liberare lo slot.
    • La sessione è andata in timeout all'avvio.
    • Il server ha spostato la sessione da questo runner.
    • Il polling del runner ha rilevato un'archiviazione o un'eliminazione prima che il processo uscisse.
    • Il runner era in fase di drain.
    • La sessione ha superato il limite --kill-session-after-min.
  • abandoned: riservato per una sessione che un altro runner ha rivendicato. L'hook attualmente non si attiva in quel caso.

Se confronti le ricevute dell'hook con i contatori del ciclo di vita della sessione, aspettati che alcune ricevute interrupted vengano contate lì come completed. I contatori contano come completed un rilascio, un timeout all'avvio, uno spostamento da parte del server e un'archiviazione o eliminazione rilevata per prima dal polling del runner, perché il runner ha restituito lo slot in modo pulito.

Lo stato di uscita dell'hook non influisce mai sul risultato della sessione; un fallimento viene registrato e ignorato. Il runner attende fino a --post-session-hook-timeout-sec, 60 secondi per impostazione predefinita, ad ogni fine sessione incluso l'arresto del runner. Questo esempio salva il lavoro non committato in un ramo di salvataggio:

#!/usr/bin/env bash
set -u
export GIT_ALLOW_PROTOCOL=${GIT_ALLOW_PROTOCOL:-https:http:ssh}
IFS=':'
rc=0  # becomes 1 when a step fails; a non-zero exit makes the runner log it
# -c overrides beat repo-local settings, blocking session-written fsmonitor,
# hook-path, and gpg-program config from executing code with the hook's
# privileges. -c commit.gpgsign=false also leaves these rescue commits
# unsigned under --configure-git.
# Repo-local credential.helper and pushurl still apply, and on a runner
# before v2.1.280 so does core.sshCommand; see the note below the script
# before you give this push a credential.
g() { git -c core.fsmonitor=false -c core.hooksPath=/dev/null \
        -c commit.gpgsign=false "$@"; }
for ws in $CLAUDE_RUNNER_WORKSPACE_PATHS; do
  cd "$ws" || { rc=1; continue; }
  g add -A || rc=1
  g diff-index --cached --quiet HEAD && continue  # nothing to save
  g commit -q -m "runner snapshot: $CLAUDE_RUNNER_SESSION_ID ($CLAUDE_RUNNER_EXIT_REASON)" || { rc=1; continue; }
  g push -q origin "HEAD:refs/heads/rescue/$CLAUDE_RUNNER_SESSION_ID" || rc=1
done
exit "$rc"

La riga GIT_ALLOW_PROTOCOL nello script limita git ai remote HTTPS, HTTP e SSH. Se l'ambiente del runner imposta già un proprio elenco GIT_ALLOW_PROTOCOL non vuoto, lo script mantiene quell'elenco.

L'hook esegue il push con le credenziali git disponibili nel proprio ambiente sull'host del runner. Con l'approccio senza credenziali nell'immagine, anche quando il clone integrato passa attraverso il proxy git di Anthropic, non ce ne sono, quindi genera una credenziale di push di breve durata all'interno dell'hook prima del push: scambia il token della sessione che l'hook riceve in CLAUDE_CODE_SESSION_ACCESS_TOKEN con il tuo servizio di token, verificandolo come descritto in Verificare l'identità della sessione.

Tratta qualsiasi credenziale che il tuo hook fornisce a git come una credenziale che una sessione può ottenere, e generala in modo che non possa fare nulla di più di questo push. Il git nel tuo hook legge file di configurazione che una sessione può scrivere, e un credential helper o un filter driver indicato in uno di essi viene eseguito con i privilegi del tuo hook. Le impostazioni in quei file possono anche cambiare la destinazione di un push, qualunque remote tu indichi. Per le impostazioni git che il runner fissa nel tuo hook e quelle che lascia a quei file, consulta Configurazione git all'interno degli hook del ciclo di vita.

Hook timing quando il runner rilascia una sessione

Una sessione rilasciata può riprendere su un altro runner. Su un runner su v2.1.236 o successivo, ciò che la sessione stava facendo al rilascio decide se può riprendere prima che questo hook finisca:

  • Inattivo dopo un turno, o timeout all'avvio: il runner ferma il figlio ed esegue questo hook fino al completamento. Solo allora rilascia la sessione. Un messaggio utente inviato mentre l'hook viene eseguito non può riprendere la sessione su un altro runner prima che l'hook finisca.
  • In attesa che l'utente risponda a un prompt, come un prompt di autorizzazione: il runner rilascia la sessione per primo, quindi esegue questo hook. Un messaggio utente inviato mentre l'hook viene eseguito può riprendere la sessione su un altro runner prima che l'hook finisca.

Questo si applica ogni volta che il runner rilascia una sessione: al timeout di inattività, al momento --retire-at, e, su un runner su v2.1.260 o successivo, al limite --kill-session-after-min di una sessione. Una sessione il cui turno è terminato e che contiene solo attività in background conta come inattiva qui. Prima della v2.1.236, il runner rilasciava la sessione per primo e quindi eseguiva questo hook in entrambi i casi.

Durante un drenaggio SIGTERM, il runner mantiene il lease della sessione fino al completamento dell'hook; consultare Shutdown timing.

Configurazione git all'interno degli hook del ciclo di vita

Gli hook checkout e post-session vengono eseguiti con il token di accesso della sessione nel loro ambiente, e il git che eseguono legge file di configurazione che le sessioni possono scrivere, come ~/.gitconfig e il .git/config di un checkout. Prima dell'esecuzione di ciascuno dei due hook, il runner imposta nell'ambiente dell'hook alcune impostazioni git, tra cui quelle elencate sotto, come coppie GIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n e variabili d'ambiente git. Git le considera prioritarie rispetto a ogni file di configurazione, e si applicano solo al git eseguito dai tuoi hook, non al git della sessione. All'avvio, il runner stampa una riga [runner:git] lifecycle hooks: che mostra il percorso degli hook, i protocolli consentiti, i programmi gpg e la modalità di firma in vigore. Richiede Claude Code v2.1.280 o successivo.

  • Hook git: a meno che tu non fornisca un valore, core.hooksPath è /dev/null, quindi git salta gli hook nella .git/hooks di un repository e in qualsiasi directory di hook indicata da ~/.gitconfig. Per fornirne uno, esporta core.hooksPath come coppia GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n nell'ambiente del runner. Il runner legge anche core.hooksPath dalla configurazione git di sistema, e la usa solo quando l'utente del runner non può scrivere in quel file, nella directory indicata o nei file di hook che contiene. Quando il runner ignora un valore, una riga [runner:warn] all'avvio indica il valore e il motivo.
  • Monitor del file system: core.fsmonitor è vuoto, quindi il git nel tuo hook non esegue un programma di monitoraggio indicato da un file di configurazione.
  • Protocolli remoti: GIT_ALLOW_PROTOCOL è https:http:ssh. Un clone, un fetch o un push che usa un percorso locale, un URL file:// o un URL git:// fallisce con fatal: transport 'file' not allowed o fatal: transport 'git' not allowed.
  • Comando SSH e richiesta di credenziali: il git nel tuo hook ignora core.sshCommand e core.askPass dai file di configurazione. Per usare il tuo comando SSH, imposta GIT_SSH_COMMAND nell'ambiente del runner. Per usare un programma di richiesta delle credenziali, imposta lì GIT_ASKPASS. Le sessioni ereditano l'ambiente del runner, quindi entrambe le variabili raggiungono anche il git della sessione. Non inserire una credenziale in nessuna delle due.
  • Programmi gpg: gpg.program, gpg.openpgp.program, gpg.x509.program e gpg.ssh.program sono percorsi impostati dal runner, mai valori provenienti da un file di configurazione.
  • Firma dei commit: con --configure-git, i commit che esegui da un hook vengono firmati come la sessione. Senza il flag, commit.gpgsign e tag.gpgsign sono false.

Per modificare una di queste impostazioni, usa l'ambiente del runner o un'opzione git -c all'interno dell'hook:

  • Coppie di configurazione: una coppia GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n che esporti nell'ambiente del runner sostituisce il valore del runner per la stessa chiave. Numera le tue coppie a partire da 0 e imposta GIT_CONFIG_COUNT sul loro numero. Quando manca l'ultima coppia indicata dal conteggio, il runner ignora tutte le tue coppie e registra una riga [runner:warn] all'avvio.
  • Variabili d'ambiente git: il runner lascia GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND e GIT_ASKPASS come le imposti nel suo ambiente.
  • Opzioni git -c: un'opzione git -c all'interno dell'hook sovrascrive una coppia GIT_CONFIG_KEY_n, sia quella del runner sia la tua. Non modifica GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND o GIT_ASKPASS, che git legge prima di qualsiasi configurazione.

Il git nel tuo hook legge comunque ogni impostazione che il runner non imposta, come i credential helper, le riscritture url.*.insteadOf e i filter driver, da ogni file di configurazione, compresi quelli che le sessioni possono scrivere. Un credential helper o un filter driver indicato in uno di questi file viene eseguito come programma con i privilegi del tuo hook, e la configurazione in quei file può ancora cambiare la destinazione di un push dal tuo hook, incluso un push verso un URL che passi sulla riga di comando.

Prima della v2.1.280, il runner non impostava nessuna di queste impostazioni e, con --configure-git, un commit eseguito da un hook falliva a meno che l'hook non passasse -c commit.gpgsign=false.

command

Viene eseguito dopo il checkout ogni volta che una sessione si avvia o riprende, al posto dello spawn del figlio integrato. L'hook riceve lo stesso ambiente di uno script wrapper e dovrebbe fare exec in "$CLAUDE_RUNNER_CLAUDE_BIN" allo stesso modo. Usa l'hook command per mantenere tutta la personalizzazione in una directory di hook; usa --exec-path quando il wrapper si trova altrove. Se è impostato anche --exec-path, il flag ha la precedenza e l'hook command viene ignorato.

Sempre fare exec del binario del runner stesso piuttosto che di un claude risolto da PATH; altrimenti si sconfigge il pinning della versione.

Runner on demand

Invece di eseguire una flotta fissa, puoi avviare un runner per sessione. L'orchestratore è un sottocomando separato e senza stato che interroga Anthropic per ottenere le richieste di spawn, una per ogni sessione in coda senza un runner disponibile, ed esegue il tuo hook spawn-runner per ciascuna. Il tuo hook invia un carico di lavoro alla tua piattaforma: un Job Kubernetes, un'istanza EC2, un dispatch Nomad.

I runner on demand migliorano la gestione delle credenziali. Su una flotta fissa, il segreto dell'ambiente risiede su ogni host dei runner, che è lo stesso host che esegue le sessioni degli utenti. Con l'orchestratore, il segreto dell'ambiente rimane solo sull'host dell'orchestratore, che non esegue mai codice utente; ogni runner avviato riceve un ordine di lavoro monouso che registra esattamente un runner e poi scade.

Per avviare l'orchestratore, passa il segreto dell'ambiente e una directory degli hook contenente uno script eseguibile spawn-runner:

claude self-hosted-runner orchestrator \
  --environment-secret-file /etc/claude/environment-secret \
  --hooks-dir /etc/claude/hooks

L'orchestratore non conserva alcuno stato tra un'interrogazione e l'altra, quindi puoi eseguire due o più repliche sullo stesso ambiente per garantire la disponibilità. Ogni richiesta di spawn viene rivendicata lato server da esattamente una replica. Tutte le repliche devono usare lo stesso valore di --expected-spawn-seconds; consulta il contratto dell'hook.

L'hook spawn-runner

L'orchestratore esegue ${hooks-dir}/spawn-runner una volta per ogni richiesta di spawn. L'hook deve inviare il lavoro in modo asincrono, senza attendere l'avvio del runner, e terminare entro --hook-timeout, 60 secondi per impostazione predefinita. L'hook riceve:

Variabile Descrizione
CLAUDE_RUNNER_WORK_ORDER_FILE Percorso di un file temporaneo contenente il JWT firmato dell'ordine di lavoro con cui si registra il nuovo runner. Viene eliminato dopo l'uscita dell'hook. Non registrare nei log il contenuto del file.
CLAUDE_RUNNER_ORDER_ID Chiave di idempotenza opaca, univoca per ogni richiesta di spawn e sicura per i nomi delle risorse Kubernetes. Usa solo ed esclusivamente l'ID dell'ordine come chiave di deduplicazione del tuo provisioner.
CLAUDE_RUNNER_SESSION_ID La sessione a cui si riferisce questa richiesta, come ID session_... presente nell'URL della sessione. Si ripete a ogni nuova richiesta per la sessione, quindi usala per il logging e l'instradamento, non come chiave di deduplicazione. Vuota per le richieste di pre-riscaldamento, che avviano un runner in standby prima di qualsiasi sessione specifica quando è impostato --min-idle, quindi non dare per scontato che la variabile sia impostata.
CLAUDE_RUNNER_SESSION_UUID Lo stesso ID di sessione in forma UUID canonica. Vuota per le richieste di pre-riscaldamento.
CLAUDE_RUNNER_ATTEMPT Un contatore per sessione da usare per il logging. Non è un conteggio dei nuovi tentativi né un conteggio delle richieste. 0 per le richieste di pre-riscaldamento, anche se pure una richiesta per una sessione può riportare 0.
CLAUDE_RUNNER_ORDER_SERVER_TIME Ora del server ricavata dall'header HTTP Date della risposta all'interrogazione. Quando l'hook verifica il campo exp del JWT dell'ordine di lavoro, confronta con questo valore anziché con l'orologio locale per tollerare le differenze di orario. Vuota quando il gateway ha omesso l'header.
CLAUDE_RUNNER_POOL_ID L'ID dell'ambiente a cui il nuovo runner deve unirsi, nella forma ccpool_...
CLAUDE_RUNNER_ACCOUNT_ID ID con tag dell'account che ha messo in coda la sessione. Usalo per l'instradamento, le quote o l'addebito per account, non come chiave di deduplicazione. Vuota quando non disponibile.
CLAUDE_RUNNER_ACCOUNT_EMAIL Email dell'account che ha messo in coda la sessione. Vuota quando non disponibile. Tratta l'email come informazione di identificazione personale e non registrarla nei log.
CLAUDE_RUNNER_PRIMARY_REPO_URL URL della prima sorgente git della sessione, per instradare verso un runner con quel repository pre-riscaldato. Vuota quando la sessione non ha sorgenti git.
CLAUDE_RUNNER_PRIMARY_REPO_REVISION Revisione della prima sorgente git della sessione: branch, SHA, tag o nome completo del riferimento. Vuota quando non specificata.
CLAUDE_RUNNER_REPO_SOURCES Array JSON di {url, revision} per tutte le sorgenti git della sessione, per gli hook che instradano in base a un repository secondario. Vuota quando non ci sono sorgenti.
CLAUDE_RUNNER_CORRELATION_ID L'ID di correlazione fornito alla creazione della sessione, restituito in modo che l'hook possa associare questo ordine di lavoro alla richiesta che ha creato la sessione. Vuota quando la sessione non ne ha uno.
CLAUDE_RUNNER_CLIENT_PLATFORM La superficie client che ha creato la sessione, come web_claude_ai, desktop_app, ios o scheduled_trigger, per le analisi di adozione. Non impostata quando la sessione non ha una superficie registrata o riconosciuta, e per le richieste di pre-riscaldamento; verificala con [ -n "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" ], che resta sicuro con set -u.
CLAUDE_RUNNER_SPAWN_REASON Il motivo per cui Anthropic ha inviato questa richiesta di spawn, come runner_lost o resumed. Non impostata quando la richiesta non riporta un motivo ben formato, quindi leggila come "${CLAUDE_RUNNER_SPAWN_REASON:-}". Richiede Claude Code v2.1.318 o successiva sull'orchestratore

Il runner avviato si registra con l'ordine di lavoro al posto del segreto dell'ambiente:

  • Avvialo con l'ordine di lavoro: fai puntare --environment-secret-file a un file contenente il JWT dell'ordine di lavoro, oppure imposta SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET sul valore del JWT.
  • Copia il JWT prima che l'hook termini: l'orchestratore elimina il file dell'ordine di lavoro dopo l'uscita dell'hook, quindi copia il JWT nel carico di lavoro che invii, ad esempio in un Secret Kubernetes sul Job avviato, anziché passare il percorso del file.
  • Usa --capacity 1 sui runner avviati: un ordine di lavoro legato a una sessione registra esattamente un runner legato a quella sessione, quindi una capacità maggiore aggiunge slot che non ricevono mai lavoro, e il runner registra un avviso all'avvio.
  • Gli ordini di lavoro di pre-riscaldamento si registrano senza vincoli: il runner in standby non è legato a una sessione e rivendica il lavoro in coda come un runner di una flotta fissa.

Il contratto prevede quattro regole, qualunque sia la piattaforma su cui il tuo hook esegue il provisioning:

  1. Sii idempotente su CLAUDE_RUNNER_ORDER_ID. La riconsegna della stessa richiesta deve avviare al massimo un runner. Ricava un nome di risorsa deterministico dall'ID dell'ordine e lascia che la tua piattaforma rifiuti il duplicato. Non usare invece CLAUDE_RUNNER_SESSION_ID come chiave. Ogni nuova richiesta per una sessione riporta lo stesso ID di sessione con un nuovo ID dell'ordine. Un carico di lavoro denominato o deduplicato in base all'ID di sessione non viene creato di nuovo finché esiste il primo.

  2. Non riprovare il carico di lavoro. Un ID dell'ordine significa al massimo un carico di lavoro creato. Se il runner non si registra mai, Anthropic invia una nuova richiesta con un nuovo ID dell'ordine dopo --expected-spawn-seconds.

  3. Usa il contratto dei codici di uscita. Esci con lo stato corrispondente all'esito:

    • Uscita 0: inviato.
    • Uscita 1, oppure ancora in esecuzione allo scadere di --hook-timeout: errore per cui è possibile riprovare. La sessione attende con backoff e viene riproposta. Dopo 10 richieste non riuscite consecutive, viene bloccata come avviene dopo un'uscita 2 o superiore.
    • Uscita 2 o superiore: errore per cui non è possibile riprovare. Alla sessione viene impedito un nuovo spawn finché un utente non le invia un nuovo messaggio o un Owner non seleziona Retry su di essa nella scheda Activity dell'ambiente.

    Mentre una sessione è in backoff, nella scheda Activity appare come Queued senza errori di spawn, quindi leggi l'output dell'hook nel log dell'orchestratore. Una volta bloccata, la sessione appare come Failed. Espandila per leggere l'errore di spawn, ricavato dagli ultimi 512 caratteri dello stderr dell'hook. Scrivi su stderr un errore utile per intervenire, e nessun segreto. In un hook shell, mantieni ripetibili gli errori transitori.

    Una richiesta di pre-riscaldamento non ha una sessione da far fallire: l'orchestratore registra un'uscita diversa da zero solo localmente, e il server richiede di nuovo lo spawn dopo la scadenza del lease --expected-spawn-seconds.

  4. Imposta --expected-spawn-seconds almeno al tuo p99 del tempo che intercorre tra la richiesta di spawn e la registrazione del runner. Misura a partire dal momento in cui l'orchestratore riceve la richiesta di spawn, e includi sia l'eventuale attesa di capacità sulla tua piattaforma sia il tempo di avvio. Questo valore è il lease lato server, e l'ordine di lavoro scade insieme a esso, quindi un runner il cui carico di lavoro impiega più tempo non può registrarsi. Tutte le repliche dell'orchestratore devono usare lo stesso valore.

Tutto ciò che l'hook scrive su stdout o stderr compare nel log dell'orchestratore con le credenziali oscurate automaticamente. Se le sessioni restano in coda, controlla il corpo di /healthz dell'orchestratore per i conteggi della coda, quindi apri la scheda Activity del tuo ambiente nella pagina di amministrazione Cloud environments: lì espandi una sessione non riuscita per vederne l'errore di spawn e seleziona Retry per richiederla di nuovo.

Correggi le sessioni riaperte che restano Queued o appaiono come Failed

Se le sessioni riaperte restano Queued o appaiono come Failed, oppure il tuo hook va in timeout su di esse, una delle possibili cause è un hook che denomina o deduplica i carichi di lavoro in base all'ID di sessione. Una sessione viene riaperta quando il suo utente le invia un messaggio dopo che il suo runner è terminato. Ad esempio, considera un hook che costruisce il nome di ogni carico di lavoro in questo modo:

name="runner-${CLAUDE_RUNNER_SESSION_ID}"
  1. Alla prima richiesta di spawn della sessione, l'hook crea un carico di lavoro con quel nome e un runner si avvia
  2. Il runner termina, ad esempio dopo --release-idle-session-min, e in seguito l'utente riapre la sessione
  3. La successiva richiesta di spawn della sessione riporta lo stesso ID di sessione e un nuovo ID dell'ordine, quindi l'hook costruisce lo stesso nome
  4. Se la tua piattaforma ha ancora il carico di lavoro della prima richiesta, perché nulla lo ha rimosso quando il suo runner è terminato, l'hook non crea alcun nuovo carico di lavoro e nessun runner si avvia
  5. Ogni successiva richiesta di spawn per la sessione termina allo stesso modo

Apri il tuo hook e leggi cosa fa quando il carico di lavoro esiste già, ad esempio quando la tua piattaforma rifiuta il nome duplicato. Individua quel comportamento nella prima colonna per vedere cosa mostra la sessione riaperta nella scheda Activity:

Quando il carico di lavoro esiste, il tuo hook Queued diventa Failed Errore di spawn
Esce con 0 senza creare nulla Dopo 10 richieste di questo tipo consecutive runner did not register within ETA after max attempts
Esce con 1 Dopo 10 richieste di questo tipo consecutive La parte finale dello stderr del tuo hook, oppure exit 1 se non ha scritto nulla su stderr
Esce con 2 o superiore Immediatamente La parte finale dello stderr del tuo hook, oppure exit seguito dal codice, ad esempio exit 2, se non ha scritto nulla su stderr
Attende fino allo scadere di --hook-timeout Dopo 10 richieste di questo tipo consecutive La parte finale dello stderr del tuo hook, oppure spawn-runner hook timed out after 60000ms se non ha scritto nulla su stderr

Nell'errore di spawn dell'ultima riga, 60000 è il tuo --hook-timeout in millisecondi. Quando un hook attende fino allo scadere di --hook-timeout, il log dell'orchestratore riceve anche una riga timed out after.

Per correggere l'hook, costruisci ogni nome o chiave a partire da CLAUDE_RUNNER_ORDER_ID, come richiede la regola 1 del contratto dell'hook. Nell'esempio, il nome diventa:

name="runner-${CLAUDE_RUNNER_ORDER_ID}"

Se il tuo hook attende, fai anche in modo che esca una volta inviato il carico di lavoro, perché un hook in attesa ritarda gli hook successivi del suo orchestratore. Dopo aver modificato l'hook, controlla lo stato di ogni sessione bloccata nella scheda Activity:

  • Queued: la sessione riceve autonomamente la sua prossima richiesta di spawn
  • Failed: seleziona Retry su di essa come Owner, oppure chiedi al suo utente di inviarle un nuovo messaggio

Fai terminare i runner on demand quando sono inattivi

Per impostazione predefinita, un runner avviato dal tuo hook non termina quando finisce il turno della sua sessione, quindi il carico di lavoro che hai inviato continua a essere eseguito. Per far terminare i runner, fai in modo che il tuo hook passi questi flag del runner:

Flag, con un valore di esempio A chi passarlo Cosa fa
--release-idle-session-min 30 Ogni runner avviato dal tuo hook Il runner rilascia la sua sessione dopo quel numero di minuti di inattività e poi esce con 0. La sessione riprende quando il suo utente invia il messaggio successivo, ma il lavoro non ancora inviato con push va perso. Mantieni la sessione per i processi che ha lasciato in esecuzione.
--kill-session-after-min 480 Ogni runner avviato dal tuo hook Limita ogni sessione a quel numero di minuti, come rete di sicurezza per le sessioni che non vengono considerate inattive, quindi impostalo al di sopra della tua sessione più lunga prevista
--exit-if-unused-min 60 Runner in standby, che il tuo hook avvia per le richieste di pre-riscaldamento Un runner in standby che non prende mai una sessione termina dopo quel numero di minuti. Se imposti --min-idle, ottieni un sostituto a meno che il runner in standby non fosse in eccesso, quindi usa un valore ben superiore al tuo tempo di avvio.

Rimuovi il carico di lavoro quando il runner termina, ad esempio impostando ttlSecondsAfterFinished su un Job Kubernetes o facendo in modo che lo script di avvio della VM elimini la VM al termine.

Invia l'output di un processo in background a un file

Se il tuo hook spawn-runner lascia un processo in esecuzione in background, invia l'output di quel processo a un file che l'utente dell'hook può creare. Ad esempio, aggiungi >> "$HOME/claude-runner.log" 2>&1 al comando che avvia il processo, prima di un eventuale &. Questa riga dell'hook mostra dove va il reindirizzamento, con your-background-command al posto del comando che il tuo hook esegue in background:

your-background-command >> "$HOME/claude-runner.log" 2>&1 &

Senza il reindirizzamento, il processo scrive su stdout e stderr dell'hook:

  • Circa un secondo dopo l'uscita dell'hook, l'orchestratore registra exited, but a process it started still has the hook's stdout or stderr open. Da quella riga in poi, registra fino a 64 KB aggiuntivi di quell'output sotto [runner:hook:spawn-runner:after-exit jti=<order id>] e poi scarta il resto.
  • Una volta terminato l'orchestratore, le scritture del processo su quei flussi falliscono.

Un runner che il tuo hook avvia direttamente esegue le sessioni sull'host dell'orchestratore, che contiene il segreto dell'ambiente.

Prima della v2.1.305, un hook già uscito raggiungeva comunque --hook-timeout mentre un processo da esso avviato teneva aperti stdout o stderr dell'hook, come mostrato da:

  • Scheda Activity, nell'errore di spawn di una sessione non riuscita: spawn-runner hook timed out after 60000ms, oppure le ultime righe sullo stderr dell'hook.
  • Log dell'orchestratore: timed out after 60000ms, sending SIGTERM to process tree.

Termina l'hook senza attendere il runner

Se ogni runner on demand si arresta circa 60 secondi dopo l'avvio del suo hook, e il tuo hook viene eseguito di nuovo per la stessa sessione, controlla se l'hook attende il runner. I 60 secondi sono il valore predefinito di --hook-timeout, quindi se imposti un tuo valore, i runner si arrestano dopo quel tempo dall'avvio dell'hook.

Un hook che avvia claude self-hosted-runner sull'host dell'orchestratore e lo attende, in primo piano o con wait, è ancora in esecuzione allo scadere di --hook-timeout. L'orchestratore arresta quindi l'hook e i processi in esecuzione sotto di esso, inclusi quel runner e il processo Claude della sessione, perciò un turno ancora in corso viene interrotto.

  • Per verificare che sia il timeout ad arrestare i tuoi runner: cerca nel log dell'orchestratore timed out after 60000ms, sending SIGTERM to process tree a ogni richiesta di spawn. Il valore 60000 è il tuo --hook-timeout in millisecondi.
  • Per risolvere il problema: fai in modo che l'hook invii il runner alla tua piattaforma ed esca con 0. Se invece aumenti --hook-timeout, il runner si arresta solo più tardi.

Mantieni ripetibili gli errori transitori in un hook shell

In un hook shell che usa set -e, un errore che un nuovo tentativo avrebbe potuto risolvere può bloccare la sessione. L'hook si ferma al comando che fallisce ed esce con lo stato di quel comando, e l'orchestratore applica il contratto dei codici di uscita a quello stato. Molti errori restituiscono uno stato pari a 2 o superiore, come 127 quando un comando non è installato e 22 da curl --fail in caso di errore HTTP, quindi bloccano la sessione al primo errore.

Una sessione che l'hook ha già bloccato resta bloccata finché un utente non le invia un nuovo messaggio o un Owner non seleziona Retry su di essa nella scheda Activity dell'ambiente.

Per trasformare invece un errore di questo tipo in un'uscita 1, inserisci queste righe subito sotto la riga #! dell'hook, prima di qualsiasi istruzione che possa fallire:

set -e
PERMANENT=; permanent() { printf '%s\n' "$*" >&2; PERMANENT=1; exit 2; }
trap 'rc=$?; [ "$rc" -eq 0 ] || [ -n "${PERMANENT:-}" ] || exit 1' EXIT

Queste righe cambiano il comportamento del resto dell'hook, quindi dopo averle aggiunte controllalo per ciascuno di questi schemi:

  • exit 2 o superiore isolato: con il trap impostato, diventa un'uscita 1. Per un errore che nessun nuovo tentativo può risolvere, chiama invece permanent con il motivo, ad esempio permanent "namespace claude-runners does not exist". Chiamalo nella shell principale, non all'interno di $( ), ( ) o di una pipe.

  • exec: non iniziare l'ultimo comando dell'hook con exec, perché exec sostituisce la shell e il trap non viene eseguito.

  • Secondo trap EXIT: un secondo trap ... EXIT sostituisce il primo, quindi unisci i due in un unico trap. Inserisci i tuoi comandi di pulizia subito dopo rc=$?; e termina ciascuno con || true;. In questo modo la pulizia viene eseguita sia in caso di errore sia in caso di successo, e un comando di pulizia che fallisce non imposta lo stato di uscita dell'hook. Questo trap unificato mostra la struttura, con your-cleanup-command al posto del tuo comando:

    trap 'rc=$?; your-cleanup-command || true; [ "$rc" -eq 0 ] || [ -n "${PERMANENT:-}" ] || exit 1' EXIT
    
  • Comandi a cui è consentito fallire: se prima l'hook non usava set -e, ora si ferma al primo comando che restituisce un valore diverso da zero, come una ricerca che non trova nulla. Se l'hook agisce in base al risultato, rendi quel comando la condizione di un if. Se ignora il risultato, fai seguire al comando || true.

  • Il comando che invia il carico di lavoro: non farlo seguire da || true. Con || true, l'hook esce con 0 anche quando la tua piattaforma rifiuta l'invio, ad esempio per superamento della quota, e la sessione attende un runner che non è mai stato avviato.

    Assegna al carico di lavoro un nome basato su CLAUDE_RUNNER_ORDER_ID, come indica la prima regola dell'hook. Una risposta che indica che la risorsa esiste già è quindi una riconsegna della stessa richiesta, perciò tratta solo quella risposta come un successo. Per qualsiasi altra risposta, stampala su stderr ed esci con 1, oppure chiama permanent se nessun nuovo tentativo può risolvere l'errore. Questo esempio fa proprio questo per kubectl create, con un job.yaml che contiene solo il carico di lavoro:

    if ! out=$(kubectl create -f job.yaml 2>&1); then
      case "$out" in
        *AlreadyExists*) ;;
        *) printf '%s\n' "${out:-kubectl create failed}" >&2; exit 1 ;;
      esac
    fi
    

    Il log dell'orchestratore mostra quindi quel messaggio. Se l'hook crea altri oggetti, assegna a ciascuno un nome basato sull'ID dell'ordine, usa un comando dedicato per ciascuno e tratta allo stesso modo come un successo la risposta che indica che esiste già. Per un oggetto che contiene un segreto, stampa un messaggio fisso al posto dell'output del comando.

Per verificare che il trap funzioni, aggiungi subito sotto la riga trap una riga che chiama un comando inesistente, come no-such-command. Esegui il file dell'hook dalla tua shell e verifica che echo $? stampi 1, quindi rimuovi la riga.

Scopri perché è stata inviata una richiesta di spawn

Il tuo hook riceve il motivo di ogni richiesta di spawn in CLAUDE_RUNNER_SPAWN_REASON, così puoi registrare nei log perché l'hook è stato eseguito, insieme a CLAUDE_RUNNER_SESSION_UUID. Richiede Claude Code v2.1.318 o successiva sull'orchestratore. Leggila come "${CLAUDE_RUNNER_SPAWN_REASON:-}", perché può non essere impostata, e avvia il runner qualunque sia il valore. Se il tuo hook si dirama in base a essa, mantieni un ramo predefinito, perché Anthropic può aggiungere valori.

Per trovare il motivo delle richieste precedenti, cerca nel log di ogni replica dell'orchestratore session= seguito dal CLAUDE_RUNNER_SESSION_UUID della sessione, non dal suo ID session_.... Le righe ok session= e nack session= mostrano il valore di CLAUDE_RUNNER_SPAWN_REASON dopo reason=, oppure reason=- se non era impostata:

  • [runner:orchestrator] ok session=: l'hook è uscito con 0
  • [runner:orchestrator] nack session=: l'orchestratore sta segnalando ad Anthropic una richiesta di spawn non riuscita

Ogni valore indica solo l'ultima causa che Anthropic ha registrato per la sessione, non la sua cronologia:

Valore Cosa è successo Cosa controllare
session_created Questa è la prima richiesta di spawn per una nuova sessione Nulla. Non farci affidamento per individuare la prima richiesta di una sessione, perché la prima richiesta che il tuo hook vede può riportare runner_lost dopo che un runner di una flotta fissa o in standby ha tenuto la sessione.
no_runner_registered Nessun runner si è registrato per la richiesta precedente entro --expected-spawn-seconds, e Anthropic non ha registrato alcuna segnalazione di errore per essa stop: no nack for session= o nack failed session= tra le righe precedenti della sessione indicano che non è stata registrata alcuna segnalazione di errore. Leggi l'eventuale errore dell'hook che le precede e, se l'orchestratore si stava arrestando, consulta Arresta o riavvia l'orchestratore. Dopo una riga ok session=, verifica che il suo runner si sia avviato e registrato entro --expected-spawn-seconds. Se nessuna replica ha registrato la richiesta, controlla i tuoi orchestratori per riavvii e righe poll failed.
hook_failed Il tuo orchestratore ha segnalato che la richiesta precedente non è riuscita e può essere riprovata, ad esempio perché il tuo hook è uscito con 1 o era ancora in esecuzione allo scadere di --hook-timeout. Il valore è lo stesso quando il tuo orchestratore restituisce una richiesta ad Anthropic senza eseguire l'hook, ad esempio mentre l'orchestratore si sta arrestando. Trova l'ultima riga [runner:orchestrator] nack session= o stop: handing back session= della sessione prima di questa richiesta. L'errore di spawn inviato dall'orchestratore segue il valore di reason= e può proseguire nelle righe successive. Una riga handing back session= indica che l'orchestratore si stava arrestando e non ha eseguito l'hook, quindi nell'hook non c'è nulla da correggere.
runner_lost Il runner che teneva la sessione ha smesso di comunicare con Anthropic, ha annullato la registrazione o è stato eliminato Scopri cosa ha arrestato il runner o lo ha isolato da Anthropic, come un'espulsione del nodo, un'interruzione per memoria esaurita, il recupero di un'istanza spot o un tuo rollout o ridimensionamento
runner_reported_failure Il runner precedente ha segnalato che la sessione non è riuscita su di esso, ad esempio durante il checkout Leggi il log del runner precedente. Se molte sessioni ricevono questo valore, controlla l'immagine e la configurazione del repository.
resumed La sessione era in pausa e ora ha di nuovo bisogno di un runner. Ad esempio, il suo runner l'ha rilasciata in base a --release-idle-session-min, e in seguito ha ricevuto un nuovo messaggio. Nulla
requeued La sessione è stata rimessa in coda, ad esempio quando un Owner ha selezionato Retry o una sessione bloccata ha ricevuto un nuovo messaggio Se la stessa sessione continua a ricevere questo valore, leggi l'errore nelle sue righe nack session=. Se il suo hook è uscito con 0, leggi il log del runner.
server_retry Anthropic ha rinunciato alla richiesta precedente o all'avvio della sessione sul suo runner, e sta riprovando. Il tuo hook non ha fallito. Nulla per una singola richiesta. Se il valore continua a comparire, su una sessione o su molte, segnalalo al tuo team dell'account Anthropic.
prewarm Questa è una richiesta di pre-riscaldamento, ancora senza sessione Nulla

Arresta o riavvia l'orchestratore

Per arrestare o riavviare l'orchestratore senza interrompere gli hook spawn-runner in esecuzione, avvialo con --shutdown-drain-sec. Alla ricezione di SIGTERM, concede quindi agli hook in esecuzione fino al numero di secondi che hai impostato per terminare. Per scegliere il valore e configurare di conseguenza il tuo supervisore di processi, consulta Dimensiona --shutdown-drain-sec e il timeout di arresto dell'orchestratore.

Questo comando di avvio concede 70 secondi:

claude self-hosted-runner orchestrator \
  --environment-secret-file /etc/claude/environment-secret \
  --hooks-dir /etc/claude/hooks \
  --shutdown-drain-sec 70

L'orchestratore esce una volta terminati l'interrogazione già inviata e tutti gli hook. Allo scadere dei secondi di --shutdown-drain-sec, gli hook ancora in esecuzione ricevono SIGTERM e l'orchestratore esce. Un secondo SIGTERM o qualsiasi SIGINT fa lo stesso senza attendere. La riga di log contenente [runner:orchestrator] stop summary: conteggia gli hook e le richieste di spawn in base a come è terminato ciascuno.

Senza --shutdown-drain-sec, l'orchestratore si arresta immediatamente alla ricezione di SIGTERM o SIGINT. Ogni hook in esecuzione riceve SIGTERM, e ad Anthropic non viene comunicato nulla. Una sessione il cui runner non è mai stato avviato può quindi restare in coda finché Anthropic non la richiede di nuovo, cosa che avviene una volta trascorso --expected-spawn-seconds, 120 secondi per impostazione predefinita.

[!NOTE]

--shutdown-drain-sec richiede Claude Code v2.1.299 o successiva. Una versione precedente esce all'avvio con error: unknown flag --shutdown-drain-sec.

Dimensiona --shutdown-drain-sec e il timeout di arresto dell'orchestratore

Con le impostazioni predefinite, usa --shutdown-drain-sec 70 con un timeout di arresto di 85 secondi, ovvero terminationGracePeriodSeconds: 85 su Kubernetes o TimeoutStopSec=85 con systemd. Se hai modificato --hook-timeout, scala entrambi i valori:

  • --shutdown-drain-sec: almeno --hook-timeout più 10 secondi, in modo che un hook avviato subito prima di SIGTERM possa comunque terminare
  • Timeout di arresto: almeno --shutdown-drain-sec più 15 secondi. L'orchestratore registra questo valore all'avvio: cerca la riga che contiene shutdown drain:.

Fai in modo che il tuo supervisore di processi invii un solo SIGTERM, al solo processo dell'orchestratore, perché un secondo segnale interrompe anticipatamente l'attesa di --shutdown-drain-sec. Se lasci invariato il valore predefinito di systemd KillMode=control-group, anche gli hook ricevono SIGTERM, e un hook può terminare prima di inviare il suo carico di lavoro. Prima di modificare KillMode, controlla come il tuo hook spawn-runner avvia i runner:

  • Se il tuo hook invia il carico di lavoro a una piattaforma ed esce: esegui l'orchestratore come processo principale della unit e imposta KillMode=mixed, che invia SIGTERM solo a quel processo.
  • Se il tuo hook avvia i runner come processi nella unit stessa dell'orchestratore: KillMode=mixed non invia SIGTERM a quei runner. Systemd invia loro invece SIGKILL in un secondo momento, quindi escono senza svuotare le loro sessioni né eseguire il loro hook post-session.

Inviare le richieste al modello a Bedrock o ad Agent Platform

Se la tua organizzazione ha bisogno che le richieste al modello passino attraverso il proprio account AWS o Google Cloud, configura il runner per Amazon Bedrock o per Agent Platform di Google Cloud, in precedenza Vertex AI. Ogni sessione avviata da quel runner chiamerà quindi il modello nel tuo account cloud, con le tue credenziali cloud. Senza questa configurazione, le sessioni inviano le richieste al modello all'API di Anthropic.

Il runner continua a interrogare Anthropic per le sessioni, e ogni sessione continua a inviare il proprio flusso di eventi a api.anthropic.com. Il flusso di eventi contiene prompt, risposte e risultati degli strumenti. Il requisito relativo al piano e l'esclusione della Zero Data Retention descritti in Disponibilità e limitazioni restano validi.

Le sessioni vengono instradate a un ambiente, non a un runner, e una sessione rimessa in coda o ripresa può essere eseguita su un runner diverso. Configura allo stesso modo ogni runner dell'ambiente. Prima di iniziare, leggi cosa cambia con questi provider.

1. Prepara l'account cloud e le tue regole di egress

Configura l'accesso ai modelli, una policy o un ruolo con ambito ristretto e l'accesso alla rete:

  • Amazon Bedrock: invia i dettagli del caso d'uso, quindi crea la policy descritta in Configurazione IAM, limitando bedrock:InvokeModel e bedrock:InvokeModelWithResponseStream ai profili di inferenza usati dalle tue sessioni e ai modelli di base che li supportano
  • Agent Platform: abilita l'API e richiedi l'accesso ai modelli, quindi crea il ruolo personalizzato descritto in Configurazione IAM, con il solo aiplatform.endpoints.predict
  • Egress: consenti gli endpoint del tuo provider nelle tue regole di egress. Consulta Requisiti di rete. Se le sessioni non riescono a raggiungerli, Claude Code può continuare a riprovare per ore prima che la sessione mostri un errore.

2. Fornisci alle sessioni credenziali con ambito ristretto

Associa la policy o il ruolo del passaggio 1 a un'identità che non possa fare nient'altro. Per i metodi accettati da Claude Code, consulta Configurare le credenziali AWS e Configurare le credenziali GCP.

[!WARNING]

Chiunque riesca a far eseguire codice in una sessione, anche tramite prompt injection, può usare queste credenziali a tue spese finché restano valide. Claude Code viene eseguito all'interno della sessione, quindi la credenziale con cui chiama il modello deve essere leggibile lì.

I comandi shell eseguiti da Claude, i tuoi hook di Claude Code e i server MCP stdio ereditano l'ambiente della sessione e vengono eseguiti con lo stesso utente di Claude Code. Di conseguenza, possono leggere le variabili e i file delle credenziali.

Quando rafforzi il tuo deploy, tieni le credenziali dell'host fuori dalle sessioni, ma non puoi tenere fuori questa credenziale. Non concedere all'identità che la supporta nulla oltre alla policy o al ruolo del passaggio 1.

Verifica il metodo che scegli rispetto a questi comportamenti del runner:

  • Endpoint dei metadati: se neghi del tutto alle sessioni l'accesso all'endpoint dei metadati cloud, nemmeno le credenziali fornite da esso, come un instance profile, raggiungono Claude Code. Un'identità web basata su file, come IAM Roles for Service Accounts (IRSA) su Amazon EKS o un file di credenziali di Workload Identity Federation, non dipende da esso.
  • Rinnovo: una sessione può durare più a lungo di una credenziale, quindi usa un metodo che si rinnova da solo, come un'identità web basata su file
  • Script wrapper: i valori delle credenziali esportati dal tuo script wrapper non vengono rinnovati mentre Claude Code è in esecuzione, quindi per una sessione lunga usa un metodo che si rinnova da solo. Claude Code legge le credenziali AWS dal proprio ambiente, quindi se il tuo wrapper esporta già credenziali AWS per altri scopi, Claude Code può usarle per firmare le richieste al modello.

3. Imposta le variabili di un solo provider nell'ambiente del runner

Imposta le variabili di un solo provider nello stesso punto in cui imposti le altre variabili d'ambiente del runner, come la specifica del container o la service unit, quindi riavvia il runner. Gli esempi le mostrano come export della shell. Con i runner on demand, impostale sul workload avviato dal tuo hook spawn-runner.

Avvia questi runner con --confine-repo-settings enforce. Rifiuta le sessioni sui repository le cui impostazioni committate vengono segnalate, quindi esegui prima nella modalità predefinita warn e risolvi ciò che viene registrato nei log.

[tab] Amazon Bedrock

Sostituisci la regione con la tua:

export CLAUDE_CODE_USE_BEDROCK=1
export AWS_REGION=us-east-1

Per sapere come Claude Code determina la regione, consulta Configurare Claude Code. Per sapere quale prefisso del profilo di inferenza usa Claude Code per la tua regione, consulta Prefissi dei profili di inferenza cross-region.

[tab] Google Cloud's Agent Platform

Sostituisci la regione e l'ID del progetto con i tuoi:

export CLAUDE_CODE_USE_VERTEX=1
export CLOUD_ML_REGION=global
export ANTHROPIC_VERTEX_PROJECT_ID=YOUR-PROJECT-ID

Per scegliere una regione, consulta Configurazione della regione.

4. Verifica che le variabili abbiano raggiunto una sessione

La tua shell sull'host è un processo diverso, quindi esegui la verifica dall'interno di una sessione. Avviane una nell'ambiente e chiedi a Claude di eseguire questo comando:

env | grep -E 'CLAUDE_CODE_USE_(BEDROCK|VERTEX)'

Una riga che imposta CLAUDE_CODE_USE_BEDROCK o CLAUDE_CODE_USE_VERTEX su 1 indica che la variabile ha raggiunto la sessione. Se compaiono entrambe, Claude Code usa Amazon Bedrock. Nessun output significa che nessuna delle due l'ha raggiunta.

Il comando mostra la configurazione, non il traffico. Per confermare le richieste stesse, cercale nelle metriche o nei log delle richieste del tuo account cloud. Se invece il primo messaggio non va a buon fine, consulta la risoluzione dei problemi per Amazon Bedrock o per Agent Platform.

Cosa cambia rispetto alle sessioni sull'API di Anthropic

Una sessione che invia le richieste al modello ad Amazon Bedrock o ad Agent Platform di Google Cloud differisce da una sessione sull'API di Anthropic nei seguenti modi:

  • Policy da claude.ai: le impostazioni gestite dal server non raggiungono queste sessioni. Nemmeno le policy dell'organizzazione che un Owner imposta nelle impostazioni di amministrazione di Claude Code le raggiungono, quindi Claude Code non le applica all'interno della sessione. Inserisci le regole su cui fai affidamento nel file delle impostazioni gestite dell'immagine del runner.
  • Skill dell'account: queste sessioni non scaricano le skill abilitate per l'account claude.ai di una persona. Consulta Come viene assemblata la configurazione di ogni sessione.
  • File: i file che le persone allegano a una sessione in claude.ai o nell'app mobile o desktop non la raggiungono, e Claude non può inviare file indietro con lo strumento SendUserFile. Inserisci invece i file di input nel repository o sul runner.
  • Selezione del modello: il control plane di Anthropic invia il modello di ogni sessione e, quando una sessione viene avviata senza un modello, Claude Code usa quello predefinito per il provider. Non puoi scegliere il modello con ANTHROPIC_MODEL o ANTHROPIC_DEFAULT_MODEL nell'ambiente del runner, ma puoi fissare a cosa si risolve un alias:
    • ANTHROPIC_MODEL e ANTHROPIC_DEFAULT_MODEL: il runner le rimuove dall'ambiente che passa alle sessioni, anche se gli esempi nelle pagine dei provider impostano ANTHROPIC_MODEL.
    • Variabili per famiglia per fissare i modelli: le variabili descritte in Fissare le versioni dei modelli per Amazon Bedrock e per Agent Platform raggiungono invece le sessioni. Determinano a cosa si risolve un alias come opus, non a cosa si risolve un ID completo del modello.
  • Modelli non serviti dal tuo account: una sessione può fallire su un messaggio con un errore che indica il modello. Abilita i modelli che i tuoi sviluppatori possono scegliere, il modello in background descritto in Fissare le versioni dei modelli e il modello del classificatore usato dalla modalità auto. Su Amazon Bedrock, consenti ciascuno di essi nella tua policy.
  • Ricerca web e modalità veloce: la ricerca web non è disponibile su Amazon Bedrock e la modalità veloce non è disponibile su nessuno dei due provider. Per altre funzionalità che variano in base al provider, consulta Funzionalità della CLI che variano in base al provider.

Server MCP

Per rendere i server MCP disponibili in ogni sessione, aggiungili durante la build dell'immagine con lo stesso comando claude mcp add usato in un'installazione desktop. Se il tuo runner è un processo nudo anziché un container, esegui lo stesso comando come utente del runner sull'host, quindi riavvia il runner: legge la configurazione dell'host una sola volta all'avvio. Il flag --scope user è obbligatorio; l'ambito locale predefinito scrive sotto una chiave per directory che il runner non inserisce nelle sessioni. Ad esempio, nel tuo Dockerfile:

RUN claude mcp add --scope user sidecar -- /usr/local/bin/mcp-sidecar
RUN claude mcp add --scope user --transport http internal http://mcp-gateway.svc.cluster.local:8080

Il runner acquisisce uno snapshot della configurazione dell'host una sola volta all'avvio. Lo snapshot cattura la chiave mcpServers dal file .claude.json dell'host, che si trova accanto a ~/.claude/ anziché al suo interno, e il runner inserisce solo quella chiave nella configurazione isolata di ogni sessione; lo stato dell'account e la cronologia dei progetti vengono scartati. Per verificare che i server abbiano raggiunto le sessioni, avvia una sessione nell'ambiente e chiedi a Claude di elencare i suoi strumenti MCP; il runner registra inoltre un avviso all'avvio per ogni voce acquisita il cui type non riconosce e scarta la voce, così puoi vedere perché quel server manca dalle sessioni. Quando SELF_HOSTED_RUNNER_HOST_CONFIG_DIR è impostata, il runner legge invece .claude.json da quella directory, quindi puntare la variabile a una directory vuota disabilita anche l'inserimento dei server MCP.

Claude Code carica i server MCP anche da altre fonti:

  • Il file MCP gestito con ambito enterprise nel suo percorso di sistema standard: /etc/claude-code/managed-mcp.json sugli host runner Linux, /Library/Application Support/ClaudeCode/managed-mcp.json sugli host macOS. Usalo per flotte bloccate in cui possono essere caricati solo i server elencati dall'amministratore. Consulta controllo esclusivo con managed-mcp.json per le regole di precedenza. Quando questo file si trova sull'host del runner, Claude Code ignora i server MCP che il control plane di Anthropic fornisce a una sessione, inclusi i connettori di claude.ai, e li nomina in un avviso sullo stderr del processo figlio della sessione, che il runner registra al livello di log debug. Prima della v2.1.229, quelle sessioni terminavano all'avvio con You cannot dynamically configure MCP servers when an enterprise MCP config is present.
  • La chiave managedMcpServers nelle impostazioni gestite sull'host del runner: fornisce server HTTP e SSE senza assumere il controllo esclusivo, quindi i server delle altre fonti vengono comunque caricati. Richiede Claude Code v2.1.259 o successiva.
  • <repo>/.mcp.json: ambito di progetto. Al di fuori di un thread di progetto, esegui il commit del file nel repository e i suoi server vengono approvati automaticamente nelle sessioni cloud. In una sessione con più repository, viene caricato al massimo il file di un solo repository. Con --trust-workspace false, le sessioni ignorano i server del file tranne quando li approvano impostazioni non incluse nei commit del repository, come enabledMcpjsonServers nel file ~/.claude/settings.json dell'host. Richiede Claude Code v2.1.319 o successiva.
  • Un file --mcp-config passato dal tuo script wrapper: aggiungi il flag dopo "$@", come in exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --mcp-config /etc/claude/mcp.json. Per mantenere in esecuzione i server di quel file quando i server forniti alla sessione vengono aggiornati a sessione in corso, fai in modo che il tuo wrapper passi l'ambiente completo al processo figlio, come fa exec. Prima della v2.1.299, i server stdio, HTTP e SSE in quel file si arrestavano al primo aggiornamento di questo tipo che non li nominava.

Quando la distribuzione dei connettori è abilitata per la tua organizzazione, il control plane di Anthropic fornisce i connettori che hai configurato su claude.ai alle sessioni create in modo interattivo tramite una configurazione MCP fornita dal server, instradata attraverso api.anthropic.com. Le sessioni create in modo programmatico, come i dispatch da CLI, non ricevono i connettori; fornisci loro i server MCP tramite una qualsiasi delle altre fonti elencate in questa sezione. Il token OAuth del processo figlio non include uno scope per recuperare direttamente i connettori, quindi il processo figlio non tenta autonomamente quel recupero; la distribuzione è gestita dal server.

[!NOTE]

In una sessione che ha ricevuto un connettore, un server con lo stesso nome proveniente dal file .claude.json dell'host o dal file .mcp.json di un repository non viene connesso. Per usarli entrambi, assegna al tuo server un nome diverso.

settings.json non contiene definizioni di server MCP e nello schema delle impostazioni non esiste un campo mcpServers di primo livello. Nelle impostazioni gestite, fornisci invece i server con la chiave managedMcpServers.

Le sessioni ereditano l'ambiente del runner, quindi imposta lì ENABLE_TOOL_SEARCH per controllare la ricerca degli strumenti MCP per ogni sessione avviata da un runner; la pagina MCP descrive i valori.

Attendere i server MCP prima del primo turno

Una sessione self-hosted attende brevemente i server MCP che si stanno ancora connettendo, in due punti distinti. Un server che manca un'attesa ha i suoi strumenti assenti all'inizio del primo turno, e questi diventano disponibili in seguito senza alcun intervento da parte tua. Le due attese sono:

  • Avvio della sessione: prima che l'elenco degli strumenti venga acquisito per la prima volta, la sessione attende per impostazione predefinita fino a 5 secondi un server HTTP o SSE la cui voce imposta alwaysLoad: true, oppure tutti i server quando imposti MCP_CONNECTION_NONBLOCKING=0 nell'ambiente del runner. Altrimenti i server HTTP e SSE si connettono in background. Mentre la sessione attende qui, l'inizializzazione è più lenta. MCP_CONNECT_TIMEOUT_MS modifica il valore predefinito di 5 secondi.
  • Primo turno: dopo l'arrivo del messaggio, il primo turno attende fino a 2 secondi i server stdio che si stanno ancora connettendo. Mentre la sessione attende qui, la prima risposta è più lenta. Per modificare la durata di questa attesa, imposta CLAUDE_CODE_MCP_STARTUP_WAIT_MS nell'ambiente del runner. Non modifica quali server sono coperti dall'attesa. Richiede Claude Code v2.1.274 o successiva.

claude mcp add non ha un flag alwaysLoad. Per impostare la chiave, aggiungi invece il server con claude mcp add-json, che la accetta nel JSON del server e la scrive in .claude.json. Nel tuo Dockerfile:

RUN claude mcp add-json core '{"type":"http","url":"https://mcp.example.com/mcp","alwaysLoad":true}' --scope user

Se gli strumenti di un server non compaiono nemmeno nei turni successivi, verifica se il server ha effettivamente raggiunto la sessione, come descritto in Server MCP.

Disattivare gli strumenti di sessione integrati

Il control plane di Anthropic collega alle sessioni cloud un proprio server MCP, chiamato Claude Code Remote. Claude usa gli strumenti del server per creare attività pianificate cloud, avviare e guidare altre sessioni cloud, collegare altri repository e seguire l'attività delle pull request.

Per disattivare l'intero server, aggiungi una regola di negazione a livello di server alle tue impostazioni. Il control plane registra il server con uno di tre nomi, a seconda di come è stata creata la sessione. Claude Code confronta il nome in una regola in modo esatto, maiuscole e minuscole comprese, quindi scrivi la regola una volta per ogni nome come mostrato:

{
  "permissions": {
    "deny": [
      "mcp__Claude_Code_Remote",
      "mcp__claude-code-remote",
      "mcp__bf7c680d-5fdc-5ef4-b4a0-abadb619bf0a"
    ]
  }
}

Una regola che nomina l'intero server copre anche gli strumenti che il server acquisirà in seguito. Per disattivare un solo strumento e mantenere gli altri, aggiungi a ogni regola altri due trattini bassi e il nome dello strumento, come in mcp__Claude_Code_Remote__add_repo. Per impedire del tutto al server di connettersi anziché rimuoverne gli strumenti, aggiungi invece i tre nomi senza il prefisso mcp__ come voci serverName in deniedMcpServers.

Inserisci le regole nelle impostazioni gestite dal server per raggiungere le sessioni senza modificare il runner, oppure in ~/.claude/settings.json sul runner. Su un runner che invia le richieste al modello a Bedrock o Agent Platform, usa quel file, perché le impostazioni gestite dal server non raggiungono quelle sessioni. Permessi e approvazione degli strumenti spiega come le impostazioni sul runner raggiungono le sessioni.

Per verificare che le regole siano state applicate, avvia una sessione nell'ambiente e chiedi a Claude di elencare i suoi strumenti MCP. Claude Code rimuove uno strumento negato dal contesto di Claude, quindi gli strumenti negati non compaiono nella sua risposta.

Prompt delle sessioni per spingere il loro lavoro

Le sessioni ospitate da Anthropic eseguono un hook Stop, l'hook Claude Code che viene eseguito quando Claude finisce di rispondere, che richiede a Claude di committare e spingere il suo lavoro. Il runner non ne installa uno. Senza di esso, una sessione che termina con cambiamenti non committati lascia quel lavoro solo sul disco del runner, e il pulsante Create PR in claude.ai/code rimane inattivo fino a quando il ramo non esiste sul remoto.

L'implementazione di riferimento di seguito ha due parti. Unire il blocco delle impostazioni in ~/.claude/settings.json sull'host del runner, che il runner semina in ogni sessione, e salvare lo script come ~/.claude/hooks/stop-hook-nudge.sh sull'host del runner e renderlo eseguibile:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "timeout": 10,
            "command": "\"$CLAUDE_CONFIG_DIR/hooks/stop-hook-nudge.sh\""
          }
        ]
      }
    ]
  }
}
#!/bin/sh
# Implementazione di riferimento dello stop-hook per runner self-hosted.
#
# Sollecita Claude una volta per turno se la directory del progetto ha modifiche
# senza commit O commit senza push, in modo che il lavoro non vada perso quando
# una sessione inattiva viene rilasciata e in modo che il pulsante "Create PR" su
# claude.ai/code si attivi.
#
# Livello runner (nessuna modifica al repository): metti questo file in ~/.claude/hooks/
# sull'host del runner e fai il merge del blocco di impostazioni dello Stop-hook associato
# in ~/.claude/settings.json — il runner inserisce entrambi in ogni sessione.
# Alternativa a livello di repository: fai il commit in <repo>/.claude/hooks/ e cambia il
# percorso del comando in settings.json in $CLAUDE_PROJECT_DIR/.claude/hooks/.
#
# stdin: payload JSON dell'hook (consulta https://code.claude.com/docs/en/hooks)
# stdout: {"decision":"block","reason":"..."} per sollecitare, o nulla per consentire lo stop.

# Protezione dal rientro: l'harness imposta stop_hook_active=true quando reinvoca
# lo Stop hook dopo un blocco. Esci in modo da sollecitare solo una volta per turno.
# L'harness emette JSON compatto (nessuno spazio dopo i due punti), su cui questo
# pattern si basa; usa jq se hai bisogno di un controllo tollerante agli spazi bianchi.
in=$(cat)
case "$in" in *'"stop_hook_active":true'*) exit 0 ;; esac

d="$CLAUDE_PROJECT_DIR"

# Hardening: questo script viene eseguito fuori dalla sandbox Bash di Claude Code, quindi
# le sue chiamate git saltano alcune funzionalità che possono avviare un programma indicato
# nelle impostazioni git del checkout. Non le saltano tutte, quindi non fare affidamento solo sulla sandbox.
GIT_OPTIONAL_LOCKS=0
export GIT_OPTIONAL_LOCKS
git() { command git -c core.fsmonitor= -c core.hooksPath=/dev/null "$@"; }

# Non è un repository git → nulla da sollecitare.
git -C "$d" rev-parse --git-dir >/dev/null 2>&1 || exit 0

# Nessun remoto → "fare il push sul remoto" è impossibile; esci.
[ -z "$(git -C "$d" remote 2>/dev/null)" ] && exit 0

# Modifiche senza commit (staged, unstaged o untracked). Escludi .claude/
# interamente — le impostazioni inserite dall'operatore e lo stato di runtime scritto dalla CLI
# (lock dello scheduler, worktree, stato della routine) si trovano lì e nessuno dei due è
# "lavoro senza commit" di cui il modello deve fare il push.
s=$(git -C "$d" status --porcelain --ignore-submodules=dirty -- . ':(exclude).claude/' 2>/dev/null)
if [ -n "$s" ]; then
  printf '{"decision":"block","reason":"There are uncommitted changes in the repository. Please commit and push these changes to the remote branch."}'
  exit 0
fi

# Commit senza push. Conta i commit su HEAD non raggiungibili da alcun
# ref di tracciamento remoto o FETCH_HEAD. Questo funziona uniformemente per:
#   - checkout init+fetch (predefinito del runner: esiste solo FETCH_HEAD)
#   - checkout basati su clone (esistono origin/*)
#   - il predefinito del runner: il processo figlio parte dal branch di risultato
#     della sessione, che il runner crea dopo il checkout
#   - HEAD staccato, quando una configurazione personalizzata salta la creazione di quel branch
# Senza alcun punto di riferimento (mai eseguito il fetch), rimani silenzioso piuttosto che
# dare un falso positivo su un turno di sola lettura.
base=""
git -C "$d" rev-parse --verify -q FETCH_HEAD >/dev/null && base="FETCH_HEAD"
if [ -z "$base" ] && [ -z "$(git -C "$d" for-each-ref --count=1 refs/remotes/origin 2>/dev/null)" ]; then
  exit 0
fi
# shellcheck disable=SC2086  # $base è "" o "FETCH_HEAD", word-split intenzionale
unpushed=$(git -C "$d" rev-list HEAD --not $base --remotes=origin --count 2>/dev/null) || unpushed=0
if [ "$unpushed" -gt 0 ]; then
  branch=$(git -C "$d" symbolic-ref --short -q HEAD)
  if [ -n "$branch" ]; then
    # $branch è influenzabile da un attaccante — git-check-ref-format(1) consente `"`
    # nei nomi dei ref. `\` è vietato (regola 10) ma viene comunque sottoposto a escape
    # come difesa in profondità a basso costo.
    # Applica l'escape ai metacaratteri JSON prima di interpolarli nel payload costruito a mano,
    # in modo che un branch come x","continue":false non possa iniettare chiavi nel
    # JSON di output dell'hook che l'harness analizza. $unpushed è sicuro — il
    # controllo -gt sopra rifiuta qualsiasi cosa che non sia un semplice intero.
    branch_esc=$(printf '%s' "$branch" | sed 's/\\/\\\\/g; s/"/\\"/g')
    printf '{"decision":"block","reason":"There are %s unpushed commit(s) on branch '\''%s'\''. Please push these changes to the remote repository."}' "$unpushed" "$branch_esc"
  else
    printf '{"decision":"block","reason":"There are %s unpushed commit(s) on a detached HEAD. Please create a branch and push it to the remote repository."}' "$unpushed"
  fi
  exit 0
fi

exit 0

L'hook chiede a Claude di fare il commit e il push prima che la sessione termini, e rimane silenzioso quando la directory non è un repository git o non ha un remoto. Per una sessione con più repository, consulta cosa indica $CLAUDE_PROJECT_DIR.

Lo Stop hook si limita a sollecitare Claude. Perché il commit e il push funzionino, configura:

Lo Stop hook sollecita al massimo una volta per turno. Se il commit o il push non riesce, il turno termina comunque. Cerca l'errore di git nella sessione e correggi ciò che indica.

Su un checkout di grandi dimensioni, aumenta timeout nelle impostazioni dello Stop hook e riavvia i runner. Il git status dello script salta core.fsmonitor, quindi può impiegare più dei 10 secondi impostati lì. Un hook che va in timeout termina il turno senza alcuna sollecitazione.

Submodule e lo Stop hook

Lo Stop hook controlla su quale commit si trova ciascun submodule, non i file al suo interno:

  • Un nuovo commit in un submodule: Claude viene sollecitato finché il repository padre non lo registra, anche se .gitmodules imposta ignore = all per quel submodule
  • File senza commit in un submodule: Claude non viene sollecitato, quindi quel lavoro rimane solo sul disco del runner
  • Commit senza push in un submodule: lo Stop hook non controlla se è stato fatto il push, quindi il padre può puntare a un commit che nessun altro può recuperare

Sostituire una copia precedente dello script dello Stop hook

Cerca GIT_OPTIONAL_LOCKS nella tua copia di stop-hook-nudge.sh. Se manca, hai una copia precedente. Sostituiscila con lo script attuale ovunque tu l'abbia installata, quindi riavvia i runner. Mantieni le impostazioni dello Stop hook, salvo aumentare timeout su un checkout di grandi dimensioni.

  • Quando: se le tue sessioni vengono eseguite con la sandbox Bash attiva, sostituiscila subito. Altrimenti, sostituiscila alla prossima build dell'immagine o al prossimo riavvio del runner.
  • Cosa cambia: i file senza commit in un submodule non sollecitano più Claude. Un nuovo commit in un submodule che .gitmodules contrassegna con ignore = all ora invece lo fa.

[!WARNING]

Gli hook vengono eseguiti fuori dalla sandbox Bash, quindi le chiamate git dello script attuale saltano alcune funzionalità di git che possono avviare un programma indicato nelle impostazioni git del checkout. Non le saltano tutte. Non fare affidamento solo sulla sandbox e isola le sessioni come descritto in Rafforzare il deploy.

Permessi e approvazione degli strumenti

Una sessione self-hosted non ha un terminale collegato, quindi una richiesta di permesso senza risposta blocca il turno finché l'utente non risponde nell'interfaccia. Il control plane di Anthropic invia l'elenco degli strumenti e le regole di permesso di ogni sessione insieme al payload di lavoro; la configurazione predefinita pre-approva le chiamate agli strumenti di routine, incluso Bash, e le sessioni cloud pre-approvano le modifiche ai file indipendentemente dalla modalità. Una chiamata che nulla pre-approva genera una richiesta di permesso tramite l'interfaccia della sessione.

Impostare in modo fisso una modalità di permesso dal wrapper

[!NOTE]

Imposta in modo fisso la modalità auto solo su un ambiente i cui container di sessione funzionano con traffico di rete in uscita negato per impostazione predefinita e con il resto della sezione sul rafforzamento della sicurezza applicato. Le chiamate agli strumenti di routine, incluse le richieste di rete di Bash, vengono eseguite senza un intervento umano sia con il set predefinito di strumenti pre-approvati sia in modalità auto, quindi è il confine di rete a limitare dove queste chiamate possono arrivare.

Per avviare le sessioni in modalità auto finché claude.ai/code o un altro client non imposta una modalità di permesso per esse, impostala in modo fisso dal tuo script wrapper o dall'hook command. Il runner invoca il wrapper passando i propri flag come argomenti e Claude Code applica l'ultimo --permission-mode presente nella riga di comando, quindi aggiungi il tuo dopo "$@":

#!/bin/bash
exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --permission-mode auto

Una volta che un client imposta una modalità, la modalità che hai aggiunto non si applica più, anche dopo il riavvio della sessione sul runner. claude.ai/code, ad esempio, ne imposta una con il primo messaggio della sessione quando il suo menu a discesa della modalità mostra Auto, oppure quando qualcuno vi ha scelto una modalità.

In modalità auto, un modello classificatore separato esamina le azioni prima che vengano eseguite e blocca quelle che rifiuta. Una regola ask esplicita forza comunque una richiesta. Una chiamata bloccata da una regola deny resta bloccata in ogni modalità.

Pre-approvare strumenti specifici

Per pre-approvare strumenti specifici, aggiungi --allowed-tools con le tue regole dopo "$@" nel tuo script wrapper, ad esempio --allowed-tools "Bash(bazel *) Bash(yarn *) mcp__internal__*". I flag di tipo elenco come --allowed-tools e --disallowed-tools si accumulano tra le occorrenze invece di sovrascriversi, quindi le tue regole si applicano in aggiunta a quelle inviate dal control plane. Per restringere, aggiungi --disallowed-tools, che nega gli strumenti anche se un'altra regola li consente.

Come viene assemblata la configurazione di ogni sessione

Il runner assegna a ogni sessione la propria directory di configurazione, popolata a partire da uno snapshot della ~/.claude/ dell'host che il runner acquisisce una sola volta all'avvio: settings.json, CLAUDE.md, hook, agenti, comandi e skill presenti nella tua immagine del runner si applicano a ogni sessione come base a livello utente. Se modifichi la configurazione su un host in esecuzione, la modifica ha effetto solo dopo il riavvio del runner.

Imposta SELF_HOSTED_RUNNER_HOST_CONFIG_DIR per popolare a partire da un percorso diverso, oppure puntala a una directory vuota per disabilitare il popolamento.

Le sessioni leggono anche questi file di impostazioni:

Per l'ordine in cui si applicano queste fonti, consulta precedenza delle impostazioni.

Quando il control plane di Anthropic fornisce a una sessione degli hook di Claude Code, il runner li installa accanto alla tua configurazione, non al suo posto. Richiede Claude Code v2.1.229 o versioni successive; i runner meno recenti ignorano gli hook forniti. Gli hook forniti eseguono python3, quindi installa python3 nella tua immagine del runner.

  • Dove vengono collocati: il runner scrive ogni script hook fornito in una sottodirectory riservata hooks/.ccr-launcher/ della directory di configurazione della sessione e registra gli script in un file di impostazioni separato che passa alla sessione con --settings, lasciando intatti il settings.json popolato e i tuoi script in hooks/<name>. Il runner ricrea la sottodirectory riservata per ogni sessione e non copia nelle sessioni il contenuto dell'host presente in ~/.claude/hooks/.ccr-launcher/.
  • Chi li crea: il control plane popola gli script a partire da costanti fisse nel proprio deploy, mai da input per sessione o di terze parti.
  • Cosa continua a regolarli: gli hook forniti tramite --settings entrano nella normale configurazione degli hook unita, non nel livello gestito, quindi le tue impostazioni gestite continuano ad applicarsi. disableAllHooks li disabilita, e non rientrano tra le categorie che allowManagedHooksOnly mantiene caricate.

Quando una persona avvia una propria sessione, Claude Code scarica le skill abilitate per il suo account claude.ai nella directory di configurazione di quella sessione. L'esecuzione di un'attività pianificata cloud non riceve le skill del proprietario, e una sessione che invia richieste al modello a Bedrock o Agent Platform non ne scarica nessuna. Per una skill di cui queste sessioni hanno bisogno, aggiungila alla tua immagine del runner oppure, al di fuori di un thread di un progetto, fanne il commit in .claude/skills/ del repository.

Al di fuori delle sessioni di Claude Tag, una sessione in un ambiente self-hosted viene eseguita con la memoria automatica disattivata per impostazione predefinita. Per le istruzioni che devono persistere tra le sessioni, usa il CLAUDE.md nella tua immagine del runner o nel repository.

Lo snapshot della ~/.claude/ dell'host acquisito dal runner esclude la directory projects/. La posizione di archiviazione predefinita della memoria automatica si trova in quella directory. Se vi inserisci file di memoria, il runner non li copia nelle sessioni e questi non attivano la memoria automatica.

Bloccare le sessioni che non riescono a recuperare le impostazioni gestite dal server

Per impedire l'avvio di una sessione che recupera le impostazioni gestite dal server quando tale recupero non riesce, imposta forceRemoteSettingsRefresh su true nel file managed-settings.json della tua immagine del runner. Una risposta dell'API di Anthropic secondo cui la tua organizzazione non ha impostazioni gestite dal server conta come recupero riuscito.

Prima di aggiungere la chiave, verifica se l'ambiente esegue sessioni di canali Claude Tag o invia richieste al modello a Bedrock o Agent Platform:

  • Sessioni di canali Claude Tag: ometti la chiave sui runner di un ambiente verso cui un Owner instrada i canali Claude Tag. L'API di Anthropic rifiuta la richiesta di impostazioni gestite dal server di queste sessioni, quindi con la chiave impostata non riescono ad avviarsi. Inserisci le loro regole nel managed-settings.json dell'immagine del runner, che continuano a leggere. Per usare la chiave per le altre sessioni, eseguile in un ambiente separato.
  • Bedrock o Agent Platform: le sessioni su questi provider non recuperano le impostazioni gestite dal server, quindi si avviano indipendentemente dal fatto che tu imposti o meno la chiave. Applica invece le tue regole dallo stesso managed-settings.json.

Questo managed-settings.json imposta la chiave:

{
  "forceRemoteSettingsRefresh": true
}

Imposta la chiave in quel file anche se la imposti pure nella console di amministrazione di claude.ai. Una nuova sessione non ha una copia nella cache delle tue impostazioni gestite dal server, quindi se imposti la chiave solo nella console, la sessione non la possiede ancora. Se poi il suo recupero non riesce, la sessione viene eseguita senza quelle impostazioni finché un recupero successivo non riesce.

Per confermare che un recupero delle impostazioni non riuscito sia il motivo per cui una sessione non si è avviata, controlla l'ultimo output di Claude Code nella sessione o il log del runner. Include Your organization requires remote managed settings to load, but they could not be loaded.

Impostazioni del repository nelle sessioni con più repository

In un thread di un progetto, nessun .claude/settings.json di alcun repository ha effetto come impostazioni di progetto e non vengono caricati né il .mcp.json né le skill di alcun repository, ovunque il thread si avvii. Per fornire una skill a questi thread, aggiungila alla tua immagine del runner.

In qualsiasi altra sessione con più repository, Claude Code legge le impostazioni di progetto dalla directory in cui si avvia la sessione, quindi al massimo il .claude/settings.json di un solo repository ha effetto come impostazioni di progetto. Un hook definito nel file di un altro repository non viene eseguito, una regola deny al suo interno non si applica e il suo env non viene impostato.

  • --capacity 1, il valore predefinito, con il checkout integrato: la sessione si avvia nel primo repository del suo elenco di repository. Il .claude/settings.json di quel repository ha effetto come impostazioni di progetto e il suo .mcp.json viene caricato, mentre quelli degli altri repository no.
  • Un --capacity maggiore di uno, oppure un hook checkout: la sessione si avvia in una directory per sessione che contiene i checkout. Nessun .claude/settings.json di alcun repository ha effetto come impostazioni di progetto, nessun .mcp.json viene caricato e $CLAUDE_PROJECT_DIR in un comando hook corrisponde a quella directory, non a un checkout.

Il CLAUDE.md di ogni repository viene caricato ovunque si avvii la sessione. Al di fuori di un thread di progetto, vengono caricate anche le sue skill. Il runner passa ogni repository a Claude Code come directory aggiuntiva, quindi Claude Code legge anche le chiavi enabledPlugins ed extraKnownMarketplaces dal .claude/settings.json di ogni repository.

Per eseguire un hook o applicare una regola di permesso in ogni sessione, inseriscilo in ~/.claude/settings.json sull'host del runner. Il runner copia il file dell'host in ogni sessione, ovunque si avvii la sessione. Scrivi un percorso in una regola Read o Edit come pattern assoluto con // o relativo alla home con ~/, perché gli altri pattern sono ancorati alla fonte delle impostazioni o alla directory corrente.

Regole di permesso di cui è stato fatto il commit nel repository

Non inserire una voce "Edit", "Write" o "NotebookEdit" senza qualificatori in un permissions.allow di cui è stato fatto il commit nel repository. Una regola per strumenti sui file senza qualificatori corrisponde allo strumento indipendentemente dal percorso, concedendo scritture ovunque sull'host anziché solo nel workspace, quindi la protezione di confinamento dell'ambito di scrittura del runner segnala la sessione; con --confine-repo-settings enforce si rifiuta di avviare la sessione invece di registrare l'evento e proseguire. Consulta la sezione sul rafforzamento della sicurezza.

Un repository non ha bisogno di alcuna regola per strumenti sui file: le sessioni cloud pre-approvano le modifiche ai file indipendentemente dalla modalità. Se fai comunque il commit di una regola, limitala al workspace, ad esempio "Edit(/**)"; una singola barra iniziale è relativa alla radice del progetto, che è il workspace della sessione. Le regole per strumenti sui file senza qualificatori vanno bene nel settings.json a livello di host dell'operatore, poiché di quel file non viene fatto il commit nel repository.

Un defaultMode impostato su auto viene rispettato solo dal file di impostazioni a livello di immagine o a livello utente, quindi un repository di cui è stato eseguito il checkout non può concedersi da solo la modalità auto. Per sapere quali modalità accettano le sessioni cloud e per la sintassi completa delle regole, consulta modalità di permesso.

Cosa c'è dopo