Personalizar sessões em ambientes auto-hospedados
Personalize sessões de ambientes auto-hospedados com scripts wrapper para credenciais por sessão, hooks de ciclo de vida e geração de runners sob demanda.
[!NOTE]
Ambientes auto-hospedados estão em beta público em planos Team e Enterprise; um Owner os habilita ativando Allow self-hosted environments na página de administração Cloud environments. Esta página assume um runner funcionando; consulte o guia de início rápido para configuração e Deploy to production para as receitas de frota.
Um ambiente auto-hospedado executa sessões na nuvem do Claude Code em sua própria infraestrutura, executadas por um processo runner que você implanta. Sem configuração, esse runner clona o repositório da sessão, gera Claude Code e limpa. Esta página é para o engenheiro de plataforma operando os runners: ela cobre os pontos de extensão para quando esses padrões não se encaixam, desde provisionamento de credenciais por sessão até substituição completa do checkout. Wrappers e hooks são executados como arquivos executáveis no host do runner, que é Linux ou macOS, e os exemplos nesta página assumem um shell POSIX.
Algumas variáveis de ambiente de hook nesta página ainda usam pool, como CLAUDE_RUNNER_POOL_ID; os nomes de flag CLI e variável de ambiente usam environment, como --environment-secret-file.
Wrapper scripts
Use um script wrapper quando cada sessão precisar de configuração que o runner não consegue fazer por conta própria: provisionamento de credenciais de curta duração com escopo para o criador da sessão, exportação de segredos específicos do ambiente, preparação de cadeias de ferramentas de linguagem ou aplicação de limites de recursos ao redor do processo filho. O runner inicia seu wrapper no lugar do binário Claude Code, uma vez por sessão. Termine o wrapper com exec em $CLAUDE_RUNNER_CLAUDE_BIN, o binário próprio do runner, para que sinais e códigos de saída se propaguem corretamente.
Aponte --exec-path, ou SELF_HOSTED_RUNNER_EXEC_PATH, para o wrapper quando você inicia o runner, substituindo <writable-dir> conforme descrito no guia de início rápido:
claude self-hosted-runner --environment-secret-file /etc/claude/environment-secret --base-dir '<writable-dir>' --exec-path /etc/claude/session-wrapper.sh
O runner define o seguinte no ambiente do wrapper:
| Variável | Descrição |
|---|---|
CLAUDE_CODE_SESSION_ACCESS_TOKEN |
O JWT da sessão, prefixado com sk-ant-cc-. Sua reivindicação act identifica o criador da sessão, com o email do criador quando a superfície criadora o registrou. O valor é o token no momento do spawn; atualizações chegam pela stdin do filho, então um wrapper vê apenas o valor inicial. Consulte Verify session identity. |
CCR_SESSION_ACCOUNT_EMAIL |
O email do criador da sessão, pré-extraído pelo runner da reivindicação act.email do token sem verificação de assinatura. Adequado para rotulagem, como trailers de commit. Quando o email controla a emissão de credenciais, verifique o token e leia a reivindicação dele em vez disso. Consulte Provision credentials scoped to the session creator. Não definido quando o token não carrega email do criador, por exemplo em sessões que a identidade de serviço da sua organização cria. Trate como informação de identificação pessoal. |
CLAUDE_RUNNER_CLIENT_PLATFORM |
A superfície do cliente que criou a sessão, como web_claude_ai, desktop_app, ios, claude_code_cli ou scheduled_trigger. Anthropic registra o valor uma vez na criação da sessão, então o wrapper e cada hook de ciclo de vida veem o mesmo valor. Use-o apenas para análise de adoção e rotulagem, não como sinal de autorização. Não definido quando a sessão não tem superfície registrada ou reconhecida. Requer Claude Code v2.1.229 ou posterior. |
CLAUDE_RUNNER_CLAUDE_BIN |
Caminho absoluto para o binário Claude Code próprio do runner. Termine seu wrapper com exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" para passar o controle para o binário fixado sem codificar um caminho de instalação. |
CLAUDE_CODE_REMOTE_SESSION_ID |
ID da sessão na forma marcada cse_.... Esta é a mesma sessão que os lifecycle hooks veem como CLAUDE_RUNNER_SESSION_ID na forma session_...; as variáveis UUID correspondem em ambos, e substituir o prefixo cse_ por session_ produz o ID mostrado na URL da sessão. |
CLAUDE_CODE_REMOTE_SESSION_UUID |
O mesmo ID da sessão na forma UUID canônica, para sistemas que usam UUIDs como chave. |
CLAUDE_CODE_REMOTE_SLACK_THREAD_URL |
Para uma sessão do Claude Tag que pertence a uma thread do Slack, o link para essa thread. Não definido para outras sessões, e pode não estar definido também para uma sessão de thread. |
CLAUDE_CODE_REMOTE_SLACK_THREAD_TS |
Para uma sessão do Claude Tag que pertence a uma thread do Slack, o timestamp do Slack dessa thread, como 1700000000.000100. Pode não estar definido, e pode estar definido quando CLAUDE_CODE_REMOTE_SLACK_THREAD_URL não está, então verifique cada variável separadamente. |
CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID |
Para uma sessão do Claude Tag, o ID do canal do Slack ou da mensagem direta que a sessão atende ou em que sua thread está, como C0123ABCDE. Um timestamp do Slack identifica uma mensagem dentro de um único canal, então registre este ID junto com CLAUDE_CODE_REMOTE_SLACK_THREAD_TS. Não definido para outras sessões, e pode não estar definido também para uma sessão do Claude Tag. |
CLAUDE_SESSION_INGRESS_TOKEN_FILE |
Caminho absoluto para um arquivo por sessão contendo o JWT da sessão atual, mantido atualizado em atualizações de token. Subprocessos shell o leem para seu cabeçalho Authorization ao baixar anexos que o usuário adicionou à sessão. exec preserva a variável automaticamente; um wrapper que reconstrói o ambiente do filho deve levar a variável, ou downloads de anexos param silenciosamente de funcionar. |
CLAUDE_CONFIG_DIR |
Diretório de configuração Claude por sessão, escrito no início da sessão a partir do snapshot da configuração do host do runner que o runner captura na inicialização; consulte Permissions and tool approval. Escritas aqui são isoladas para esta sessão. O diretório fica sob <base-dir>/_sessions/ após o término da sessão, a menos que você inicie o runner com --remove-session-state; consulte Reuse a pre-warmed checkout. |
CLAUDE_CODE_REMOTE_MCP_CONFIG |
Caminho para um arquivo em CLAUDE_CONFIG_DIR contendo as definições de servidores MCP que o plano de controle da Anthropic entregou para esta sessão. Se seu wrapper reconstrói o ambiente do filho, leve esta variável junto. Sem ela, quando o plano de controle atualiza os servidores entregues, o filho encerra qualquer servidor stdio, HTTP ou SSE que seu wrapper adiciona com --mcp-config que não esteja na atualização. Requer Claude Code v2.1.299 ou posterior. |
ANTHROPIC_BASE_URL |
A URL base da API que o filho usará, entregue pelo plano de controle por sessão e normalmente https://api.anthropic.com. Não a sobrescreva: a credencial de inferência da sessão é um token OAuth emitido pela Anthropic que outros provedores não aceitam. |
CLAUDE_CODE_OAUTH_TOKEN |
O token de acesso OAuth de curta duração que o filho usa para inferência de modelo, com uma vida útil de cerca de 30 minutos. O runner o re-emite antes da expiração e entrega a rotação pela stdin do filho, então um wrapper que não mantém stdin anexado vê apenas o valor inicial. Não confie na allowlist de IP da sua organização para limitar o uso deste token: trate-o como uma credencial de portador que permanece utilizável por aproximadamente 30 minutos se vazar, e não o registre em log, não o escreva em disco nem o encaminhe para fora do contêiner da sessão. |
O wrapper também herda o resto do ambiente gerenciado do filho, incluindo quaisquer variáveis de ambiente fornecidas pelo servidor. exec propaga tudo automaticamente; se seu wrapper gera o filho de outra forma, encaminhe o ambiente completo.
Você pode ler as variáveis CLAUDE_CODE_REMOTE_SLACK_* no seu wrapper ou hook command, e no que a sessão executa, como comandos shell, hooks do git e hooks do Claude Code. Os hooks checkout, post-session e spawn-runner não as recebem.
Give a default to variables that can be unset
Se seu script usa set -u, expanda 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 com um valor padrão, como "${CCR_SESSION_ACCOUNT_EMAIL:-}". Cada uma pode não estar definida, e sob set -u o Bash para com unbound variable ao expandir uma que não está definida.
Quote the arguments and variables your script expands
Coloque entre aspas cada argumento e variável que o runner passa para seu wrapper ou hook command. Alguns valores contêm caracteres que um shell interpreta, porque contêm JSON, frases inteiras ou uma regra de permissão como Bash(npm run test *).
- Argumentos: repasse-os como
"$@" - Variáveis: coloque cada uma entre aspas, como em
"${CLAUDE_CODE_REMOTE_SLACK_THREAD_URL:-}". O link da thread do Slack, por exemplo, pode conter?e&. - Strings de
evalesh -c: não substitua um valor em uma string queevaloush -cexecuta, mesmo entre aspas. Em vez disso, faça essa string referenciar a variável.
Keep stdin and file descriptor 3 attached
A stdin do filho é o canal de controle do runner. Rotações de token e sinais de fim de sessão chegam nela. O runner também abre um pipe no descritor de arquivo 3 e lê sinais de atividade do filho dele para conduzir timeouts de inatividade e inicialização. Um simples exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" preserva ambos automaticamente.
Se seu wrapper coloca o filho em background com um simples &, ele corta a stdin do filho. A sessão parece saudável até a vida útil do token OAuth inicial de aproximadamente 30 minutos expirar, e então cada chamada de API que usa o token falha com 401 authentication_error. Se seu wrapper deve colocar o filho em background, por exemplo para manter uma trap de teardown viva, salve stdin no descritor de arquivo 9 e re-anexe-a explicitamente:
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"
Você pode redirecionar o stdout do filho. Mantenha o descritor de arquivo 3 e o stderr anexados ao runner:
- Descritor de arquivo 3: transporta os sinais de atividade do filho para o runner. Não o feche nem o reutilize no wrapper.
- Descritores de arquivo 4 e 5: runners no Claude Code v2.1.309 ou posterior os reservam para o filho. Não os feche nem os reutilize no wrapper. Se seu wrapper usa algum deles, por exemplo para salvar stdin, mova esse uso para um descritor de 6 a 9, que versões anteriores também deixam livres.
- stderr: quando o wrapper ou o filho sai com código diferente de zero, o runner publica as últimas linhas do stderr na sessão e as imprime em seu próprio log. O usuário da sessão vê essas linhas, então não imprima segredos no stderr e remova
set -xantes de implantar o wrapper. Se você redirecionar o stderr, as sessões ainda são executadas, mas o runner relata uma falha apenas com o código de saída.
Pass the system prompt flags through
O system prompt e o system prompt anexado que o plano de controle da Anthropic envia para uma sessão chegam ao seu wrapper como caminhos de arquivo, não como texto inline. O runner grava cada prompt em um arquivo no diretório de configuração da sessão, CLAUDE_CONFIG_DIR, e passa seu caminho nos argumentos que seu wrapper recebe, como --system-prompt-file <path> ou --append-system-prompt-file <path>.
Runners no Claude Code v2.1.281 ou posterior entregam os prompts como arquivos. Antes da v2.1.281, o runner os passava como --system-prompt <text> e --append-system-prompt <text>.
No seu script wrapper ou hook command, trate essas flags da seguinte forma:
- Repasse-as: termine o wrapper com
exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@", que encaminha as flags de arquivo junto com todos os outros argumentos. Não as descarte nem as reescreva. Se uma sessão perder uma flag de arquivo de prompt, ela será executada sem as instruções que o plano de controle enviou para ela. - Em um runner na v2.1.281 ou posterior, uma flag de arquivo que você acrescenta substitui a do servidor, nunca se soma a ela: cada flag de arquivo de prompt aceita um único valor e o Claude Code mantém a última ocorrência, então se você acrescentar
--append-system-prompt-file <path>depois de"$@", o conteúdo do seu arquivo substitui as instruções anexadas do servidor. Para adicionar instruções além das do servidor, coloque-as noCLAUDE.mdda imagem do runner, que o runner insere na configuração de nível de usuário de cada sessão.
Fix Argument list too long at session start
No Linux, se você juntar o prompt do plano de controle e seu próprio texto em um único argumento --append-system-prompt, o runner registra em log uma linha como esta assim que o argumento atinge 128 KB:
Session <id> finished: failed — reason: child exited 126: /opt/wrapper.sh: line 9: /usr/local/bin/claude: Argument list too long
O Linux limita cada argumento individual a 32 páginas de memória, ou 128 KB com as páginas usuais de 4 KB. O valor de getconf ARG_MAX, frequentemente 2 MB, é um limite separado para todos os argumentos e o ambiente juntos.
No Claude Code v2.1.283 ou posterior, deixe as flags de arquivo em "$@" e passe apenas seu próprio texto como --append-system-prompt. O Claude Code usa ambos, primeiro o prompt do plano de controle e depois seu próprio texto:
exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --append-system-prompt "$(cat /opt/team-instructions.md)"
No Linux, se seu próprio texto atingir 128 KB, mova-o para o CLAUDE.md da imagem do runner, porque ele ainda é um único argumento.
Na v2.1.281 ou v2.1.282, esse comando sai com Cannot use both --append-system-prompt and --append-system-prompt-file. Em versões anteriores, seu texto substitui o prompt do plano de controle sem nenhum erro. Em qualquer uma delas, atualize primeiro ou mova seu próprio texto para o CLAUDE.md.
Provision credentials scoped to the session creator
Use o subcomando decode-token para ler reivindicações do JWT da sessão. Ele lê o token de um argumento, de CLAUDE_CODE_SESSION_ACCESS_TOKEN ou de stdin, nessa ordem; consulte Verify the token inside the session para o que ele verifica. O exemplo abaixo decodifica a identidade do criador, a troca por credenciais AWS de curta duração e faz exec em 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" "$@"
Use jq -re em vez de jq -r quando a reivindicação extraída controla uma decisão de autenticação, para que uma reivindicação ausente saia com código diferente de zero em vez de passar a string literal null para downstream. Sessões criadas por uma identidade de serviço da organização, como sessões de bot e agente, carregam um assunto agent: em vez de user:, então este exemplo as recusa; se seu ambiente serve essas sessões, decida explicitamente se o wrapper volta para uma credencial padrão para elas em vez de sair. Quando sua troca de credenciais precisa do email em vez disso, leia .act.email e trate sua ausência: o token o carrega apenas quando a superfície criadora o registrou, e uma sessão despachada por CLI pode não tê-lo. Para a referência de reivindicação completa e verificação de serviços fora do runner, consulte Verify session identity.
Lifecycle hooks
Lifecycle hooks substituem estágios do pipeline por sessão do runner com seus próprios scripts. Aponte o runner para um diretório de hooks com --hooks-dir <path>, ou SELF_HOSTED_RUNNER_HOOKS_DIR. O runner procura por arquivos executáveis com nomes bem conhecidos; qualquer hook que não esteja presente cai para o comportamento integrado, então você só escreve os que precisa. Hooks são executados com os privilégios próprios do runner, e filhos de sessão compartilham esse UID, então monte o diretório de hooks como somente leitura, ou coloque-o na imagem, para que o código da sessão não possa modificá-lo; consulte a seção de hardening.
Esses hooks são distintos dos Claude Code hooks, que são executados dentro da sessão; lifecycle hooks são executados no runner, ao redor da sessão.
checkout
Executado uma vez por repositório, no lugar do clone e fetch integrados do runner. Use o hook para clonar de um espelho de leitura que você acessa por HTTPS ou SSH, semear uma árvore de trabalho a partir de um arquivo ou aplicar autenticação git por sessão. O runner define estas variáveis, e pode definir outras variáveis CLAUDE_RUNNER_ que a tabela não lista:
| Variável | Descrição |
|---|---|
CLAUDE_RUNNER_REPO_URL |
URL do repositório para clonar, após qualquer --git-host-rewrite e --git-ssh-rewrite terem sido aplicados |
CLAUDE_RUNNER_REPO_REF |
Revisão para fazer checkout, conforme a sessão a solicitou: um branch, tag, commit SHA ou nome de referência completo como refs/heads/<branch> ou refs/pull/<number>/head. Vazio significa o branch padrão do repositório. |
CLAUDE_RUNNER_CHECKOUT_PATH |
Caminho absoluto onde a árvore de trabalho deve ser deixada |
CLAUDE_RUNNER_SESSION_ID |
ID da sessão na forma marcada session_..., para logging e correlação |
CLAUDE_RUNNER_SESSION_UUID |
O mesmo ID da sessão na forma UUID canônica |
CLAUDE_RUNNER_API_BASE_URL |
URL base da API Anthropic para chamadas com escopo de sessão |
CLAUDE_RUNNER_CLIENT_PLATFORM |
A superfície do cliente que criou a sessão, como web_claude_ai, desktop_app ou ios. Não definido quando a sessão não tem superfície registrada ou reconhecida, então referencie-a como "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" sob set -u. Requer Claude Code v2.1.229 ou posterior. |
CLAUDE_CODE_SESSION_ACCESS_TOKEN |
O token de acesso da sessão, para chamadas de API com escopo de sessão |
GIT_CONFIG_COUNT, GIT_CONFIG_KEY_n, GIT_CONFIG_VALUE_n |
Configurações git que o runner fixa para o git que seu hook executa. Configuração do Git dentro de lifecycle hooks as descreve. Requer Claude Code v2.1.280 ou posterior. |
O script deve deixar uma árvore de trabalho em CLAUDE_RUNNER_CHECKOUT_PATH com checkout na revisão solicitada. Um HEAD desanexado funciona, porque o runner cria o branch de trabalho da sessão em cima.
Depois que seu hook retorna, o runner verifica se CLAUDE_RUNNER_CHECKOUT_PATH contém um .git. Se seu hook materializa uma fonte não-git como Perforce ou um tarball desempacotado, defina CLAUDE_RUNNER_SKIP_GIT_VERIFY=1 no ambiente do runner para pular essa verificação. Fluxos baseados em Git como criação de branch de trabalho e push de resultados requerem um checkout git, então exporte resultados de árvores não-git com um hook post-session.
Obter credenciais git no hook
O runner não passa uma credencial git para o hook. O subcomando decode-token também não está disponível aqui, porque CLAUDE_RUNNER_CLAUDE_BIN não está definido no ambiente do checkout-hook. Em vez disso, emita uma credencial de clone por sessão a partir da identidade da sessão, ou recorra à própria autenticação git do host:
- Credencial de clone por sessão: verifique
CLAUDE_CODE_SESSION_ACCESS_TOKENcom uma biblioteca JWT padrão contra o endpoint JWKS sobCLAUDE_RUNNER_API_BASE_URL, conforme descrito em Verify the token from your service. Em seguida, faça seu serviço de credencial emitir uma credencial de clone de curta duração para a identidade na reivindicaçãoactdo token. Associe essa credencial aact.sub, e não exijaact.email. - Autenticação git do host: use qualquer autenticação git que o host já tenha, como um agente SSH, credential helper ou
.netrc.
Quando o hook falha
O hook falha quando sai com código diferente de zero, ou sai com 0 sem deixar um checkout utilizável para trás:
-
Um repositório para o qual a sessão faz push de resultados: o runner falha a sessão, e em uma saída diferente de zero exibe a cauda do stderr do script para o usuário. Escreva o motivo no stderr como uma única linha curta, escreva-a por último e então saia com código diferente de zero. Não imprima segredos no stderr e remova
set -xantes de implantar o hook.Se o
couldn't find remote refdo git aparecer aproximadamente nos últimos 4 KB do stderr do hook, o runner relata um branch excluído em vez disso. A mensagem de erro da sessão então diz que o branch não existe mais no remoto e pede ao usuário que o restaure. Se esse branch existir, consulte Uma sessão falha comno longer exists on the remoteoucouldn't find remote ref. -
Um repositório que a sessão apenas lê, como um repositório adicionado a uma sessão em execução: o runner o pula e continua com os repositórios restantes. O runner publica um passo
Skippedpara a sessão e remove o que o hook deixou no caminho de checkout.Quando o passo
Skippedmenciona o checkout hook, encontre o detalhe da falha na linha[runner:warn] checkout hook failedno log do runner. Quando o passo diz que uma ref de origemno longer exists on remote, consulte Uma sessão falha comno longer exists on the remoteoucouldn't find remote ref. Se pular deixa a sessão sem nenhum repositório, o runner falha a sessão mesmo assim.
Quando o hook é bem-sucedido, o runner remove o caminho de checkout após a sessão terminar.
post-session
Executado uma vez por sessão, após o filho Claude Code ter saído e antes do runner desmontar o workspace. Este hook é sua única chance de salvar trabalho não confirmado: em --capacity acima de um, o runner deleta worktrees por sessão logo após o hook retornar, e em --capacity 1 o clone canônico reutilizado é hard-reset quando a próxima sessão começa, então mudanças rastreadas não confirmadas não sobrevivem em nenhum caminho. Usos típicos são fazer push de um branch de snapshot de mudanças não confirmadas, arquivar logs ou emitir um evento de fim de sessão para seus próprios sistemas.
O hook dispara em cada fim de sessão onde um processo filho foi gerado, qualquer que seja a causa; os valores CLAUDE_RUNNER_EXIT_REASON abaixo enumeram os casos. Não pode disparar quando o runner termina abruptamente, como uma preempção de VM ou perda de energia; se você precisa de garantias contra terminação abrupta, faça snapshot periodicamente de dentro da sessão com um hook Claude Code PostToolUse em vez disso. O runner define:
| Variável | Descrição |
|---|---|
CLAUDE_RUNNER_SESSION_ID |
ID da sessão na forma marcada session_... |
CLAUDE_RUNNER_SESSION_UUID |
O mesmo ID da sessão na forma UUID canônica |
CLAUDE_RUNNER_EXIT_REASON |
Como a sessão terminou; consulte os valores abaixo da tabela |
CLAUDE_RUNNER_WORKSPACE_PATHS |
Caminhos absolutos separados por dois-pontos das árvores de trabalho da sessão. Vazio para sessões sem repositório. |
CLAUDE_RUNNER_DEBUG_LOG_PATH |
Caminho para o log de debug da sessão, ainda em disco enquanto o hook é executado |
CLAUDE_RUNNER_API_BASE_URL |
URL base da API Anthropic para chamadas com escopo de sessão |
CLAUDE_RUNNER_CLIENT_PLATFORM |
A superfície do cliente que criou a sessão, como web_claude_ai, desktop_app ou ios. Não definido quando a sessão não tem superfície registrada ou reconhecida, então referencie-a como "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" sob set -u. Requer Claude Code v2.1.229 ou posterior. |
CLAUDE_CODE_SESSION_ACCESS_TOKEN |
O token de acesso da sessão, para chamadas de API com escopo de sessão |
GIT_CONFIG_COUNT, GIT_CONFIG_KEY_n, GIT_CONFIG_VALUE_n |
Configurações git que o runner fixa para o git que seu hook executa. Configuração do Git dentro de lifecycle hooks as descreve. Requer Claude Code v2.1.280 ou posterior. |
CLAUDE_RUNNER_EXIT_REASON toma um de quatro valores:
completed: a sessão terminou de forma limpa. O processo Claude Code saiu normalmente, ou saiu por conta própria depois que a sessão foi arquivada ou deletada.failed: o processo Claude Code travou, ou a configuração falhou após ele ter iniciado.interrupted: o runner parou a sessão, em um destes casos:- O runner liberou a sessão para liberar o slot.
- A sessão atingiu o timeout na inicialização.
- O servidor moveu a sessão para fora deste runner.
- A verificação periódica do runner detectou um arquivamento ou exclusão antes de o processo sair.
- O runner estava drenando.
- A sessão ultrapassou seu limite
--kill-session-after-min.
abandoned: reservado para uma sessão que outro runner reivindicou. O hook não dispara atualmente nesse caso.
Se você comparar recibos de hook com os contadores de ciclo de vida da sessão, espere que alguns recibos interrupted contem como completed ali. Os contadores contam uma liberação, um timeout de inicialização, uma movimentação de servidor e um arquivamento ou exclusão que a verificação periódica do runner detectou primeiro como completed, porque o runner devolveu o slot de forma limpa.
O status de saída do hook nunca afeta o resultado da sessão; uma falha é registrada e ignorada. O runner aguarda até --post-session-hook-timeout-sec, 60 segundos por padrão, em cada fim de sessão incluindo shutdown do runner. Este exemplo salva trabalho não confirmado para um branch de resgate:
#!/usr/bin/env bash
set -u
export GIT_ALLOW_PROTOCOL=${GIT_ALLOW_PROTOCOL:-https:http:ssh}
IFS=':'
# -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" 2>/dev/null || continue
[ -z "$(g status --porcelain 2>/dev/null)" ] && continue
g add -A
g commit -q -m "runner snapshot: $CLAUDE_RUNNER_SESSION_ID ($CLAUDE_RUNNER_EXIT_REASON)" || continue
g push -q origin "HEAD:refs/heads/rescue/$CLAUDE_RUNNER_SESSION_ID" || true
done
A linha GIT_ALLOW_PROTOCOL no script limita o git a remotos HTTPS, HTTP e SSH. Se o ambiente do runner já define uma lista GIT_ALLOW_PROTOCOL própria não vazia, o script mantém essa lista.
O hook faz push com quaisquer credenciais git disponíveis em seu próprio ambiente no host do runner. Sob a postura de sem-credenciais-na-imagem, incluindo quando o clone integrado passa pelo proxy git Anthropic, não há nenhuma, então emita uma credencial de push de curta duração dentro do hook antes de fazer push: troque o token de sessão que o hook recebe em CLAUDE_CODE_SESSION_ACCESS_TOKEN com seu próprio serviço de token, verificando-o conforme Verify session identity descreve.
Trate qualquer credencial que seu hook fornece ao git como uma que uma sessão pode obter, e emita-a de modo que ela não possa fazer mais do que este push. O git em seu hook lê arquivos de configuração que uma sessão pode escrever, e um credential helper ou filter driver nomeado em um deles é executado com os privilégios do seu hook. Opções de configuração nesses arquivos também podem alterar para onde vai um push, qualquer que seja o remoto que você nomeie. Para as configurações git que o runner fixa em seu hook e as que ele deixa para esses arquivos, consulte Configuração do Git dentro de lifecycle hooks.
Hook timing when the runner releases a session
Uma sessão liberada pode retomar em outro runner. Em um runner na v2.1.236 ou posterior, o que a sessão estava fazendo na liberação decide se ela pode retomar antes deste hook terminar:
- Inativo após uma volta, ou expirado na inicialização: o runner para o filho e executa este hook até a conclusão. Apenas então ele libera a sessão. Uma mensagem do usuário enviada enquanto o hook é executado não pode retomar a sessão em outro runner antes do hook terminar.
- Aguardando o usuário responder a um prompt, como um prompt de permissão: o runner libera a sessão primeiro, então executa este hook. Uma mensagem do usuário enviada enquanto o hook é executado pode retomar a sessão em outro runner antes do hook terminar.
Isso se aplica sempre que o runner libera uma sessão: no timeout de inatividade, no tempo --retire-at, e, em um runner na v2.1.260 ou posterior, no limite --kill-session-after-min de uma sessão. Uma sessão cuja volta terminou e que mantém apenas tarefas em background conta como inativa aqui. Antes da v2.1.236, o runner liberava a sessão primeiro e então executava este hook em ambos os casos.
Durante uma drenagem SIGTERM, o runner mantém a concessão da sessão até o hook terminar; consulte Shutdown timing.
Configuração do Git dentro de lifecycle hooks
Os hooks checkout e post-session são executados com o token de acesso da sessão em seu ambiente, e o git que eles executam lê arquivos de configuração que as sessões podem escrever, como ~/.gitconfig e o .git/config de um checkout. Antes de qualquer um dos hooks ser executado, o runner define configurações git no ambiente do hook, incluindo as listadas abaixo, como pares GIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n e variáveis de ambiente do git. O git as classifica acima de todo arquivo de configuração, e elas se aplicam apenas ao git que seus hooks executam, não ao git da própria sessão. Na inicialização, o runner imprime uma linha [runner:git] lifecycle hooks: que mostra o caminho de hooks, os protocolos permitidos, os programas gpg e o modo de assinatura em vigor. Requer Claude Code v2.1.280 ou posterior.
- Git hooks: a menos que você forneça um valor,
core.hooksPathé/dev/null, então o git ignora os hooks no.git/hooksde um repositório e qualquer diretório de hooks que~/.gitconfignomeie. Para fornecer um, exportecore.hooksPathcomo um parGIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_nno ambiente do runner. O runner também lêcore.hooksPathda configuração git do sistema, e o usa apenas quando o usuário do runner não pode escrever nesse arquivo, no diretório que ele nomeia ou nos arquivos de hook dentro dele. Quando o runner ignora um valor, uma linha[runner:warn]na inicialização nomeia o valor e o motivo. - Monitor do sistema de arquivos:
core.fsmonitoré vazio, então o git em seu hook não executa um programa de monitor que um arquivo de configuração nomeie. - Protocolos remotos:
GIT_ALLOW_PROTOCOLéhttps:http:ssh. Um clone, fetch ou push que usa um caminho local, uma URLfile://ou uma URLgit://falha comfatal: transport 'file' not allowedoufatal: transport 'git' not allowed. - Comando SSH e prompt de credencial: o git em seu hook ignora
core.sshCommandecore.askPassde arquivos de configuração. Para usar seu próprio comando SSH, definaGIT_SSH_COMMANDno ambiente do runner. Para usar um programa de prompt de credencial, definaGIT_ASKPASSali. As sessões herdam o ambiente do runner, então ambas as variáveis também chegam ao git da própria sessão. Não coloque uma credencial em nenhuma delas. - Programas gpg:
gpg.program,gpg.openpgp.program,gpg.x509.programegpg.ssh.programsão caminhos que o runner define, nunca valores de um arquivo de configuração. - Assinatura de commits: com
--configure-git, os commits que você faz a partir de um hook são assinados como a sessão. Sem a flag,commit.gpgsignetag.gpgsignsãofalse.
Para alterar uma dessas configurações, use o ambiente do runner ou uma opção git -c dentro do hook:
- Pares de configuração: um par
GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_nque você exporta no ambiente do runner substitui o valor do runner para a mesma chave. Numere seus pares a partir de0e definaGIT_CONFIG_COUNTcom a quantidade deles. Quando o último par que a contagem anuncia está ausente, o runner ignora todos os seus pares e registra uma linha[runner:warn]na inicialização. - Variáveis de ambiente do git: o runner deixa
GIT_ALLOW_PROTOCOL,GIT_SSH_COMMANDeGIT_ASKPASScomo você as definiu em seu ambiente. - Opções
git -c: uma opçãogit -cdentro do hook sobrescreve um parGIT_CONFIG_KEY_n, seja do runner ou seu. Ela não alteraGIT_ALLOW_PROTOCOL,GIT_SSH_COMMANDouGIT_ASKPASS, que o git lê antes de qualquer configuração.
O git em seu hook ainda lê toda opção de configuração que o runner não define, como credential helpers, reescritas url.*.insteadOf e filter drivers, de todo arquivo de configuração, incluindo os que as sessões podem escrever. Um credential helper ou filter driver nomeado em um desses arquivos é executado como um programa com os privilégios do seu hook, e a configuração nesses arquivos ainda pode alterar para onde vai um push do seu hook, incluindo um push para uma URL que você passa na linha de comando.
Antes da v2.1.280, o runner não definia nenhuma dessas configurações, e sob --configure-git um commit feito a partir de um hook falhava a menos que o hook passasse -c commit.gpgsign=false.
command
Executado uma vez por sessão após checkout, no lugar do spawn do filho integrado. O hook recebe o mesmo ambiente que um wrapper script e deve fazer exec em "$CLAUDE_RUNNER_CLAUDE_BIN" da mesma forma. Use o hook command para manter toda a customização em um diretório de hooks; use --exec-path quando o wrapper vive em outro lugar. Se --exec-path também está definido, a flag tem precedência e o hook command é ignorado.
Sempre faça exec do binário próprio do runner em vez de um claude resolvido por PATH; caso contrário você derrota o pinning de versão.
On-demand runners
Em vez de executar uma frota fixa, você pode inicializar um runner por sessão. O orquestrador é um subcomando separado e sem estado que faz polling na Anthropic para solicitações de spawn, uma por sessão que está enfileirada sem runner disponível, e executa seu hook spawn-runner para cada uma. Seu hook submete uma carga de trabalho para sua plataforma: um Kubernetes Job, uma instância EC2, um Nomad dispatch.
Runners sob demanda melhoram a higiene de credenciais. Em uma frota fixa, o segredo do ambiente vive em cada host do runner, que é o mesmo host que executa sessões do usuário. Com o orquestrador, o segredo do ambiente fica apenas no host do orquestrador, que nunca executa código do usuário; cada runner gerado recebe uma ordem de trabalho de uso único que registra exatamente um runner e depois expira.
Para iniciar o orquestrador, passe o segredo do ambiente e um diretório de hooks contendo um script spawn-runner executável:
claude self-hosted-runner orchestrator \
--environment-secret-file /etc/claude/environment-secret \
--hooks-dir /etc/claude/hooks
O orquestrador não mantém estado entre polls, então você pode executar duas ou mais réplicas contra o mesmo ambiente para disponibilidade. Cada solicitação de spawn é reivindicada no lado do servidor por exatamente uma réplica. Todas as réplicas devem usar o mesmo valor --expected-spawn-seconds; consulte o contrato do hook.
The spawn-runner hook
O orquestrador executa ${hooks-dir}/spawn-runner uma vez por solicitação de spawn. O hook deve submeter trabalho de forma assíncrona, sem aguardar o boot do runner, e retornar dentro de --hook-timeout, 60 segundos por padrão. O hook recebe:
| Variável | Descrição |
|---|---|
CLAUDE_RUNNER_WORK_ORDER_FILE |
Caminho para um arquivo temporário contendo o JWT da ordem de trabalho assinada que o novo runner se registra. Deletado após o hook sair. Não registre o conteúdo do arquivo. |
CLAUDE_RUNNER_ORDER_ID |
Chave de idempotência opaca, única por solicitação de spawn e segura para nomes de recursos Kubernetes. Use apenas o ID de ordem como sua chave de dedup do provisionador. |
CLAUDE_RUNNER_SESSION_ID |
A sessão para a qual esta solicitação é, como o ID session_... na URL da sessão. Ela se repete em cada re-solicitação para a sessão, então use-a para logging e roteamento, não como chave de dedup. Vazio para solicitações de pré-aquecimento, que inicializam um runner em standby antes de qualquer sessão específica quando --min-idle está definido, então não assuma que a variável está definida. |
CLAUDE_RUNNER_SESSION_UUID |
O mesmo ID da sessão na forma UUID canônica. Vazio para solicitações de pré-aquecimento. |
CLAUDE_RUNNER_ATTEMPT |
Um contador por sessão para usar em logging. Não é uma contagem de novas tentativas nem uma contagem de solicitações. 0 para solicitações de pré-aquecimento, embora uma solicitação para uma sessão também possa carregar 0. |
CLAUDE_RUNNER_ORDER_SERVER_TIME |
Hora do servidor do cabeçalho HTTP Date da resposta de poll. Quando o hook verifica o exp do JWT da ordem de trabalho, compare contra este valor em vez do relógio local para tolerar skew. Vazio quando o gateway omitiu o cabeçalho. |
CLAUDE_RUNNER_POOL_ID |
O ID do ambiente que o novo runner deve se juntar, na forma ccpool_... |
CLAUDE_RUNNER_ACCOUNT_ID |
ID marcado da conta que enfileirou a sessão. Use-o para roteamento por conta, quota ou chargeback, não como chave de dedup. Vazio quando indisponível, e sempre vazio para sessões do canal Claude Tag, que nenhuma conta enfileira. |
CLAUDE_RUNNER_ACCOUNT_EMAIL |
Email da conta que enfileirou a sessão. Vazio quando indisponível. Trate o email como informação de identificação pessoal e não o registre. |
CLAUDE_RUNNER_PRIMARY_REPO_URL |
URL da primeira fonte git da sessão, para roteamento para um runner com esse repositório pré-aquecido. Vazio quando a sessão não tem fontes git. |
CLAUDE_RUNNER_PRIMARY_REPO_REVISION |
Revisão da primeira fonte git da sessão: branch, SHA, tag ou nome completo da referência. Vazio quando não especificado. |
CLAUDE_RUNNER_REPO_SOURCES |
Array JSON de {url, revision} para todas as fontes git da sessão, para hooks que roteiam em um repositório secundário. Vazio quando não há fontes. |
CLAUDE_RUNNER_CORRELATION_ID |
O ID de correlação fornecido na criação da sessão, ecoado de volta para que o hook possa mapear esta ordem de trabalho para a solicitação que criou a sessão. Vazio quando a sessão não tem nenhum. |
CLAUDE_RUNNER_CLIENT_PLATFORM |
A superfície do cliente que criou a sessão, como web_claude_ai, desktop_app, ios ou scheduled_trigger, para análise de adoção. Não definido quando a sessão não tem superfície registrada ou reconhecida, e para solicitações de pré-aquecimento; verifique-o com [ -n "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" ], que permanece seguro sob set -u. |
O runner gerado se registra com a ordem de trabalho no lugar do segredo do ambiente:
- Inicie-o com a ordem de trabalho: aponte
--environment-secret-filepara um arquivo contendo o JWT da ordem de trabalho, ou definaSELF_HOSTED_RUNNER_ENVIRONMENT_SECRETpara o valor JWT. - Copie o JWT antes do hook sair: o orquestrador deleta o arquivo da ordem de trabalho após o hook sair, então copie o JWT para a carga de trabalho que você submete, como um Kubernetes Secret no Job gerado, em vez de passar o caminho do arquivo.
- Use
--capacity 1em runners gerados: uma ordem de trabalho vinculada a sessão registra exatamente um runner vinculado a essa sessão, então uma capacidade maior adiciona slots que nunca recebem trabalho, e o runner registra um aviso na inicialização. - Ordens de trabalho de pré-aquecimento registram desvinculadas: o runner em standby não está vinculado a uma sessão e reclama trabalho enfileirado como um runner de frota fixa.
O contrato tem quatro regras, qualquer que seja a plataforma em que seu hook provisiona:
-
Seja idempotente em
CLAUDE_RUNNER_ORDER_ID. Reentrega da mesma solicitação deve gerar no máximo um runner. Derive um nome de recurso determinístico do ID de ordem e deixe sua plataforma rejeitar a duplicata. Não useCLAUDE_RUNNER_SESSION_IDcomo chave. Cada re-solicitação para uma sessão carrega o mesmo ID de sessão com um novo ID de ordem. Uma carga de trabalho nomeada ou deduplicada pelo ID de sessão não é criada novamente enquanto a primeira existir. -
Não tente novamente a carga de trabalho. Um ID de ordem significa no máximo uma carga de trabalho criada. Se o runner nunca se registra, Anthropic re-solicita com um ID de ordem fresco após
--expected-spawn-seconds. -
Use o contrato de código de saída. Saia com o status que corresponde ao resultado:
- Saída 0: submetido.
- Saída 1, ou ainda em execução quando
--hook-timeouttermina: falha retentável. A sessão recua e é re-oferecida. Após 10 solicitações com falha seguidas, ela é bloqueada da mesma forma que após a saída 2 ou superior. - Saída 2 ou superior: falha não-retentável. A sessão é bloqueada de gerar novamente até que um usuário envie uma nova mensagem a ela ou um Owner selecione Retry nela na aba Activity do ambiente.
Enquanto uma sessão recua, ela aparece como Queued na aba Activity sem erro de spawn, então leia a saída do hook no log do orquestrador. Uma vez bloqueada, a sessão aparece como Failed. Expanda-a para ler o erro de spawn, extraído dos últimos 512 caracteres do stderr do hook. Escreva um erro acionável para stderr, e nenhum segredo. Em um hook de shell, mantenha falhas transitórias retentáveis.
Uma solicitação de pré-aquecimento não tem sessão para falhar: o orquestrador registra uma saída diferente de zero localmente apenas, e o servidor re-solicita o spawn após a concessão de
--expected-spawn-secondsexpirar. -
Defina
--expected-spawn-secondspara pelo menos seu tempo p99 desde a solicitação de spawn até o registro do runner. Meça a partir do momento em que o orquestrador recebe a solicitação de spawn e inclua qualquer espera por capacidade na sua plataforma, além do tempo de boot. Este valor é a concessão no lado do servidor, e a ordem de trabalho expira com ela, então um runner cuja carga de trabalho demora mais não consegue se registrar. Todas as réplicas do orquestrador devem usar o mesmo valor.
Tudo que o hook escreve para stdout ou stderr aparece no log do orquestrador com credenciais automaticamente redatadas. Se sessões ficarem enfileiradas, verifique o corpo /healthz do orquestrador para contagens de fila, então abra a aba Activity do seu ambiente na página de administração Cloud environments: expanda uma sessão falhada lá para seu erro de spawn e selecione Retry para re-solicitá-la.
Corrija sessões reabertas que permanecem Queued ou mostram Failed
Se sessões reabertas permanecem como Queued ou mostram Failed, ou se seu hook atinge o timeout nelas, uma causa é um hook que nomeia ou deduplica cargas de trabalho pelo ID de sessão. Uma sessão é reaberta quando o usuário envia uma mensagem a ela depois que seu runner saiu. Por exemplo, considere um hook que monta o nome de cada carga de trabalho assim:
name="runner-${CLAUDE_RUNNER_SESSION_ID}"
- Na primeira solicitação de spawn da sessão, o hook cria uma carga de trabalho com esse nome, e um runner é iniciado
- O runner sai, por exemplo após
--release-idle-session-min, e o usuário reabre a sessão mais tarde - A próxima solicitação de spawn da sessão carrega o mesmo ID de sessão e um novo ID de ordem, então o hook monta o mesmo nome
- Se sua plataforma ainda tem a carga de trabalho da primeira solicitação, porque nada a removeu quando seu runner saiu, o hook não cria nenhuma carga de trabalho nova e nenhum runner é iniciado
- Cada solicitação de spawn posterior para a sessão termina da mesma forma
Abra seu próprio hook e leia o que ele faz quando a carga de trabalho já existe, por exemplo quando sua plataforma rejeita o nome duplicado. Encontre esse comportamento na primeira coluna para ver o que a sessão reaberta mostra na aba Activity:
| Quando a carga de trabalho existe, seu hook | Queued muda para Failed | Erro de spawn |
|---|---|---|
| Sai com 0 sem criar nada | Após 10 dessas solicitações seguidas | runner did not register within ETA after max attempts |
| Sai com 1 | Após 10 dessas solicitações seguidas | O final do stderr do seu hook, ou exit 1 se ele não escreveu nada no stderr |
| Sai com 2 ou superior | Imediatamente | O final do stderr do seu hook, ou exit e o código, como exit 2, se ele não escreveu nada no stderr |
Aguarda até --hook-timeout terminar |
Após 10 dessas solicitações seguidas | O final do stderr do seu hook, ou spawn-runner hook timed out after 60000ms se ele não escreveu nada no stderr |
No erro de spawn da última linha, 60000 é o seu --hook-timeout em milissegundos. Quando um hook aguarda até --hook-timeout terminar, o log do orquestrador também recebe uma linha timed out after.
Para corrigir o hook, monte cada nome ou chave a partir de CLAUDE_RUNNER_ORDER_ID, como exige a regra 1 do contrato do hook. No exemplo, o nome passa a ser:
name="runner-${CLAUDE_RUNNER_ORDER_ID}"
Se seu hook aguarda, faça-o também sair assim que tiver submetido a carga de trabalho, porque um hook que aguarda atrasa os próximos hooks do seu orquestrador. Depois de alterar o hook, verifique o status de cada sessão travada na aba Activity:
- Queued: a sessão recebe sua próxima solicitação de spawn por conta própria
- Failed: selecione Retry nela como Owner, ou peça ao usuário dela que envie uma nova mensagem
Faça runners sob demanda saírem quando ociosos
Por padrão, um runner que seu hook inicia não sai quando o turno da sua sessão termina, então a carga de trabalho que você submeteu continua em execução. Para fazer os runners saírem, faça seu hook passar estas flags do runner:
| Flag, com um valor de exemplo | Passe-a para | O que ela faz |
|---|---|---|
--release-idle-session-min 30 |
Todo runner que seu hook inicia | O runner libera sua sessão após essa quantidade de minutos ociosos e então sai com 0. A sessão é retomada quando o usuário envia a próxima mensagem, mas o trabalho que ela não havia enviado com push é perdido. Retenha uma sessão para os processos que ela deixou em execução. |
--kill-session-after-min 480 |
Todo runner que seu hook inicia | Limita cada sessão a essa quantidade de minutos, como proteção para sessões que não contam como ociosas, então defina-a acima da sua sessão mais longa esperada |
--exit-if-unused-min 60 |
Runners em standby, que seu hook inicia para solicitações de pré-aquecimento | Um runner em standby que nunca assume uma sessão sai após essa quantidade de minutos. Se você definir --min-idle, você recebe um substituto, a menos que o runner em standby fosse excedente, então use um valor bem acima do seu tempo de boot. |
Remova a carga de trabalho quando o runner sair, por exemplo definindo ttlSecondsAfterFinished em um Kubernetes Job ou fazendo o script de inicialização da VM excluir a VM depois.
Envie a saída de um processo em segundo plano para um arquivo
Se seu hook spawn-runner deixa um processo em execução em segundo plano, envie a saída desse processo para um arquivo que o usuário do hook possa criar. Por exemplo, acrescente >> "$HOME/claude-runner.log" 2>&1 ao comando que inicia o processo, antes de qualquer &. Esta linha de hook mostra onde fica o redirecionamento, com your-background-command representando o comando que seu hook coloca em segundo plano:
your-background-command >> "$HOME/claude-runner.log" 2>&1 &
Sem o redirecionamento, o processo escreve no stdout e no stderr do hook:
- Cerca de um segundo após o hook sair, o orquestrador registra
exited, but a process it started still has the hook's stdout or stderr open. A partir dessa linha, ele registra até mais 64 KB dessa saída sob[runner:hook:spawn-runner:after-exit jti=<order id>]e então descarta o restante. - Depois que o orquestrador sai, as escritas do processo ali falham.
Um runner que seu hook inicia por conta própria executa sessões no host do orquestrador, que contém o segredo do ambiente.
Antes da v2.1.305, um hook que já havia saído ainda atingia o --hook-timeout enquanto um processo que ele iniciou mantinha o stdout ou o stderr do hook aberto, exibido como:
- Aba Activity, no erro de spawn de uma sessão com falha:
spawn-runner hook timed out after 60000ms, ou as últimas linhas no stderr do hook. - Log do orquestrador:
timed out after 60000ms, sending SIGTERM to process tree.
Saia do hook sem aguardar o runner
Se todo runner sob demanda para cerca de 60 segundos depois que seu hook inicia, e seu hook é executado novamente para a mesma sessão, verifique se o hook aguarda o runner. Os 60 segundos são o --hook-timeout padrão, então, se você definir o seu próprio, os runners param esse tempo depois que o hook inicia.
Um hook que inicia claude self-hosted-runner no host do orquestrador e aguarda por ele, em primeiro plano ou com wait, ainda está em execução quando --hook-timeout termina. O orquestrador então interrompe o hook e os processos em execução sob ele, incluindo esse runner e o processo Claude da sessão, de modo que um turno que ainda está em execução é cortado.
- Para confirmar que o timeout é o que interrompe seus runners: procure no log do orquestrador por
timed out after 60000ms, sending SIGTERM to process treeem cada solicitação de spawn. O60000é seu--hook-timeoutem milissegundos. - Para corrigir: faça o hook submeter o runner à sua plataforma e sair com 0. Se, em vez disso, você aumentar o
--hook-timeout, o runner apenas para mais tarde.
Mantenha falhas transitórias retentáveis em um hook de shell
Em um hook de shell que usa set -e, uma falha que uma nova tentativa poderia ter resolvido pode bloquear a sessão. O hook para no comando que falhou e sai com o próprio status desse comando, e o orquestrador aplica o contrato de código de saída a esse status. Muitas falhas retornam um status de 2 ou superior, como 127 quando um comando não está instalado e 22 de curl --fail em um erro HTTP, então elas bloqueiam a sessão na sua primeira falha.
Uma sessão que o hook já bloqueou permanece bloqueada até que um usuário envie uma nova mensagem a ela ou um Owner selecione Retry nela na aba Activity do ambiente.
Para transformar essa falha em saída 1, coloque estas linhas diretamente abaixo da linha #! do hook, acima de qualquer coisa que possa falhar:
set -e
PERMANENT=; permanent() { printf '%s\n' "$*" >&2; PERMANENT=1; exit 2; }
trap 'rc=$?; [ "$rc" -eq 0 ] || [ -n "${PERMANENT:-}" ] || exit 1' EXIT
Estas linhas mudam como o restante do hook se comporta, então verifique-o quanto a cada um destes padrões depois de adicioná-las:
-
exit 2ou superior isolado: com o trap definido, ele se torna saída 1. Para um erro que nenhuma nova tentativa pode corrigir, chamepermanentcom o motivo, comopermanent "namespace claude-runners does not exist". Chame-o no shell principal, não dentro de$( ),( )ou de um pipe. -
exec: não inicie o último comando do hook comexec, porqueexecsubstitui o shell e o trap não é executado. -
Segundo trap
EXIT: um segundotrap ... EXITsubstitui o primeiro, então mescle os dois em um único trap. Coloque seus comandos de limpeza diretamente apósrc=$?;e termine cada um com|| true;. A limpeza então é executada tanto em caso de falha quanto de sucesso, e um comando de limpeza que falha não define o status de saída do hook. Este trap mesclado mostra o formato, comyour-cleanup-commandrepresentando o seu próprio comando:trap 'rc=$?; your-cleanup-command || true; [ "$rc" -eq 0 ] || [ -n "${PERMANENT:-}" ] || exit 1' EXIT -
Comandos que podem falhar: se o hook não usava
set -eantes, agora ele para no primeiro comando que retorna diferente de zero, como uma consulta que não encontra nada. Se o hook age com base no resultado, faça desse comando a condição de umif. Se ele ignora o resultado, siga o comando com|| true. -
O comando que submete a carga de trabalho: não o siga com
|| true. Com|| true, o hook sai com 0 mesmo quando sua plataforma recusa a submissão, por exemplo por exceder a quota, e a sessão aguarda um runner que nunca foi iniciado.Nomeie a carga de trabalho com base em
CLAUDE_RUNNER_ORDER_ID, como diz a primeira regra do hook. Uma resposta de que o recurso já existe é então uma reentrega da mesma solicitação, então trate apenas essa resposta como sucesso. Para qualquer outra coisa, imprima a resposta no stderr e saia com 1, ou chamepermanentse nenhuma nova tentativa puder corrigi-la. Este exemplo faz isso parakubectl create, com umjob.yamlque contém apenas a carga de trabalho: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 fiO log do orquestrador então mostra essa mensagem. Se o hook cria outros objetos, nomeie cada um com base no ID de ordem, dê a cada um seu próprio comando e trate sua resposta de que o recurso já existe como sucesso da mesma forma. Para um objeto que contém um segredo, imprima uma mensagem fixa no lugar da saída do comando.
Para confirmar que o trap funciona, adicione uma linha diretamente abaixo da linha trap que chama um comando que não existe, como no-such-command. Execute o arquivo do hook a partir do seu shell e verifique se echo $? imprime 1, depois remova a linha.
Pare ou reinicie o orquestrador
Para parar ou reiniciar o orquestrador sem interromper hooks spawn-runner em execução, inicie-o com --shutdown-drain-sec. Ao receber SIGTERM, ele então concede aos hooks em execução até o número de segundos que você definir para terminarem. Para escolher o valor e configurar seu supervisor de processos de acordo, consulte Dimensione --shutdown-drain-sec e o timeout de parada do orquestrador.
Este comando de inicialização permite 70 segundos:
claude self-hosted-runner orchestrator \
--environment-secret-file /etc/claude/environment-secret \
--hooks-dir /etc/claude/hooks \
--shutdown-drain-sec 70
O orquestrador sai assim que o poll já enviado e todos os hooks tiverem terminado. Quando os segundos de --shutdown-drain-sec se esgotam, os hooks ainda em execução recebem SIGTERM e o orquestrador sai. Um segundo SIGTERM ou qualquer SIGINT faz o mesmo sem aguardar. A linha de log contendo [runner:orchestrator] stop summary: contabiliza hooks e solicitações de spawn pela forma como cada um terminou.
Sem --shutdown-drain-sec, o orquestrador para imediatamente ao receber SIGTERM ou SIGINT. Cada hook em execução recebe SIGTERM, e a Anthropic não é informada de nada. Uma sessão cujo runner nunca foi iniciado pode então ficar na fila até que a Anthropic a re-solicite, o que acontece depois que --expected-spawn-seconds tiver passado, 120 segundos por padrão.
[!NOTE]
--shutdown-drain-secrequer Claude Code v2.1.299 ou posterior. Uma versão mais antiga sai na inicialização comerror: unknown flag --shutdown-drain-sec.
Dimensione --shutdown-drain-sec e o timeout de parada do orquestrador
Com as configurações padrão, use --shutdown-drain-sec 70 com um timeout de parada de 85 segundos, ou seja, terminationGracePeriodSeconds: 85 no Kubernetes ou TimeoutStopSec=85 no systemd. Se você alterou --hook-timeout, ajuste ambos:
--shutdown-drain-sec: pelo menos--hook-timeoutmais 10 segundos, para que um hook que começou logo antes doSIGTERMainda possa terminar- Timeout de parada: pelo menos
--shutdown-drain-secmais 15 segundos. O orquestrador registra esse valor na inicialização: procure a linha que contémshutdown drain:.
Faça seu supervisor de processos enviar um único SIGTERM, apenas para o processo do orquestrador, porque um segundo encurta a espera de --shutdown-drain-sec. Se você mantiver o padrão do systemd KillMode=control-group, os hooks também recebem SIGTERM, e um hook pode morrer antes de submeter sua carga de trabalho. Antes de alterar KillMode, verifique como seu hook spawn-runner inicia runners:
- Se seu hook submete sua carga de trabalho a uma plataforma e sai: execute o orquestrador como o processo principal da unit e defina
KillMode=mixed, que enviaSIGTERMapenas para esse processo. - Se seu hook inicia runners como processos na própria unit do orquestrador:
KillMode=mixednão enviaSIGTERMa esses runners. Em vez disso, o systemd enviaSIGKILLa eles mais tarde, então eles saem sem drenar suas sessões nem executar seu hookpost-session.
Enviar requisições de modelo para Bedrock ou Agent Platform
Se a sua organização precisa que as requisições de modelo passem pela própria conta da AWS ou do Google Cloud, configure o runner para o Amazon Bedrock ou para o Agent Platform do Google Cloud, anteriormente Vertex AI. Cada sessão que esse runner iniciar passará a chamar o modelo na sua conta de nuvem, com as suas credenciais de nuvem. Sem essa configuração, as sessões enviam requisições de modelo para a API da Anthropic.
O runner continua consultando a Anthropic em busca de sessões, e cada sessão continua enviando seu fluxo de eventos para api.anthropic.com. O fluxo de eventos transporta prompts, respostas e resultados de ferramentas. O requisito de plano e a exclusão de Zero Data Retention em Disponibilidade e limitações continuam se aplicando.
As sessões são roteadas para um ambiente, não para um runner, e uma sessão recolocada na fila ou retomada pode ser executada em um runner diferente. Configure todos os runners do ambiente da mesma forma. Antes de começar, leia o que muda nesses provedores.
1. Prepare a conta de nuvem e as suas regras de saída
Configure o acesso ao modelo, uma política ou função com escopo restrito e o acesso à rede:
- Amazon Bedrock: envie os detalhes do caso de uso e, em seguida, crie a política em Configuração de IAM, limitando
bedrock:InvokeModelebedrock:InvokeModelWithResponseStreamaos perfis de inferência que as suas sessões usam e aos modelos de base por trás deles - Agent Platform: ative a API e solicite acesso ao modelo e, em seguida, crie a função personalizada descrita em Configuração de IAM, apenas com
aiplatform.endpoints.predict - Saída: permita os endpoints do seu provedor nas suas regras de saída. Consulte Requisitos de rede. Se as sessões não conseguirem alcançá-los, o Claude Code pode continuar tentando novamente por horas antes que a sessão mostre um erro.
2. Forneça às sessões credenciais com escopo restrito
Anexe a política ou função da etapa 1 a uma identidade que não possa fazer mais nada. Para os métodos que o Claude Code aceita, consulte Configurar credenciais da AWS e Configurar credenciais do GCP.
[!WARNING]
Qualquer pessoa que consiga executar código em uma sessão, inclusive por meio de injeção de prompt, pode usar essas credenciais às suas custas enquanto as credenciais forem válidas. O Claude Code é executado dentro da sessão, portanto a credencial com que ele chama o modelo precisa ser legível ali.
Os comandos de shell que o Claude executa, os seus hooks do Claude Code e os servidores MCP stdio herdam o ambiente da sessão e são executados com o mesmo usuário que o Claude Code. Como resultado, eles podem ler variáveis de credenciais e arquivos de credenciais.
Ao reforçar a segurança da sua implantação, você mantém as credenciais do host fora das sessões, mas não é possível manter esta credencial fora. Não conceda à identidade por trás dela nada além da política ou função da etapa 1.
Verifique o método escolhido em relação a estes comportamentos do runner:
- Endpoint de metadados: se você negar totalmente às sessões o acesso ao endpoint de metadados da nuvem, as credenciais fornecidas por ele, como um perfil de instância, também não chegam ao Claude Code. Uma identidade web baseada em arquivo, como IAM Roles for Service Accounts (IRSA) no Amazon EKS ou um arquivo de credencial de Workload Identity Federation, não depende dele.
- Renovação: uma sessão pode durar mais que uma credencial, portanto use um método que se renove sozinho, como uma identidade web baseada em arquivo
- Script wrapper: o runner inicia o seu script wrapper uma vez por sessão, portanto as credenciais que ele exporta não são renovadas. O Claude Code lê as credenciais da AWS do seu ambiente, então, se o seu wrapper já exporta credenciais da AWS para outro trabalho, o Claude Code pode assinar as requisições de modelo com elas.
3. Defina as variáveis de um único provedor no ambiente do runner
Defina as variáveis de exatamente um provedor no mesmo lugar em que você define as outras variáveis de ambiente do runner, como a especificação do contêiner ou a unidade de serviço, e depois reinicie o runner. Os exemplos as mostram como exports de shell. Com runners sob demanda, defina-as na carga de trabalho que o seu hook spawn-runner inicia.
Inicie esses runners com --confine-repo-settings enforce. Ele recusa sessões em repositórios cujas configurações commitadas ele sinaliza, então execute primeiro no modo padrão warn e resolva o que ele registrar em log.
Amazon Bedrock
Substitua a região pela sua:
export CLAUDE_CODE_USE_BEDROCK=1
export AWS_REGION=us-east-1
Para saber como o Claude Code resolve a região, consulte Configurar o Claude Code. Para saber qual prefixo de perfil de inferência o Claude Code usa para a sua região, consulte Prefixos de perfil de inferência entre regiões.
Agent Platform do Google Cloud
Substitua a região e o ID do projeto pelos seus:
export CLAUDE_CODE_USE_VERTEX=1
export CLOUD_ML_REGION=global
export ANTHROPIC_VERTEX_PROJECT_ID=YOUR-PROJECT-ID
Para escolher uma região, consulte Configuração de região.
4. Verifique se as variáveis chegaram a uma sessão
O seu próprio shell no host é um processo diferente, então verifique de dentro de uma sessão. Inicie uma no ambiente e peça ao Claude para executar este comando:
env | grep -E 'CLAUDE_CODE_USE_(BEDROCK|VERTEX)'
Uma linha que define CLAUDE_CODE_USE_BEDROCK ou CLAUDE_CODE_USE_VERTEX como 1 significa que a variável chegou à sessão. Se ambas aparecerem, o Claude Code usa o Amazon Bedrock. Nenhuma saída significa que nenhuma delas chegou.
O comando mostra a configuração, não o tráfego. Para confirmar as próprias requisições, procure-as nas métricas ou nos logs de requisições da sua própria conta de nuvem. Se, em vez disso, a primeira mensagem falhar, consulte a solução de problemas do Amazon Bedrock ou do Agent Platform.
O que muda em relação às sessões na API da Anthropic
Uma sessão que envia requisições de modelo para o Amazon Bedrock ou para o Agent Platform do Google Cloud difere de uma sessão na API da Anthropic das seguintes maneiras:
- Políticas do claude.ai: as configurações gerenciadas pelo servidor não chegam a essas sessões. Também não chegam as políticas da organização que um Owner define nas configurações de administração do Claude Code, portanto o Claude Code não as aplica dentro da sessão. Coloque as regras das quais você depende no arquivo de configurações gerenciadas da imagem do runner.
- Skills da conta: essas sessões não baixam as skills ativadas para a conta do claude.ai de uma pessoa. Consulte Como a configuração de cada sessão é montada.
- Arquivos: os arquivos que as pessoas anexam a uma sessão no claude.ai ou no aplicativo móvel ou desktop não chegam a ela, e o Claude não pode enviar arquivos de volta com a ferramenta
SendUserFile. Em vez disso, coloque os arquivos de entrada no repositório ou no runner. - Seleção de modelo: o plano de controle da Anthropic envia o modelo de cada sessão e, quando uma sessão é iniciada sem um, o Claude Code usa o seu padrão para o provedor. Não é possível escolher o modelo com
ANTHROPIC_MODELouANTHROPIC_DEFAULT_MODELno ambiente do runner, mas você pode fixar para o que um alias é resolvido:ANTHROPIC_MODELeANTHROPIC_DEFAULT_MODEL: o runner as remove do ambiente que passa às sessões, embora os exemplos das páginas dos provedores definamANTHROPIC_MODEL.- Variáveis de fixação por família: as variáveis em Fixar versões de modelo para o Amazon Bedrock e o Agent Platform chegam, sim, às sessões. Elas decidem para o que um alias como
opusé resolvido, não para o que um ID de modelo completo é resolvido.
- Modelos que a sua conta não disponibiliza: uma sessão pode falhar em uma mensagem com um erro que nomeia o modelo. Ative os modelos que os seus desenvolvedores podem escolher, o modelo de segundo plano descrito em Fixar versões de modelo e o modelo classificador que o modo auto usa. No Amazon Bedrock, permita cada um deles na sua política.
- Pesquisa na web e modo rápido: a pesquisa na web não está disponível no Amazon Bedrock, e o modo rápido não está disponível em nenhum dos dois provedores. Para outros recursos que variam por provedor, consulte Recursos da CLI que variam por provedor.
Servidores MCP
Para disponibilizar servidores MCP em todas as sessões, adicione-os no momento do build da imagem com o mesmo comando claude mcp add usado em uma instalação desktop. Se o seu runner for um processo simples em vez de um contêiner, execute o mesmo comando como o usuário do runner no host e, em seguida, reinicie o runner: ele lê a configuração do host uma única vez na inicialização. A flag --scope user é obrigatória; o escopo local padrão grava sob uma chave por diretório que o runner não propaga para as sessões. Por exemplo, no seu 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
O runner captura um snapshot da configuração do host uma única vez na inicialização. O snapshot captura a chave mcpServers do .claude.json do host, que fica ao lado de ~/.claude/ e não dentro dele, e o runner propaga apenas essa chave para a configuração isolada de cada sessão; o estado da conta e o histórico de projetos são descartados. Para confirmar que os servidores chegaram às sessões, inicie uma sessão no ambiente e peça ao Claude para listar suas ferramentas MCP; o runner também registra em log um aviso na inicialização para qualquer entrada capturada cujo type ele não reconheça e descarta a entrada, para que você possa ver por que esse servidor está ausente das sessões. Quando SELF_HOSTED_RUNNER_HOST_CONFIG_DIR está definida, o runner lê .claude.json desse diretório, então apontar a variável para um diretório vazio também desativa a propagação de MCP.
O Claude Code também carrega servidores MCP de outras fontes:
- O arquivo MCP gerenciado de escopo enterprise em seu caminho padrão do sistema:
/etc/claude-code/managed-mcp.jsonem hosts de runner Linux,/Library/Application Support/ClaudeCode/managed-mcp.jsonem hosts macOS. Use-o para frotas restritas em que apenas servidores listados pelo administrador podem ser carregados. Consulte controle exclusivo com managed-mcp.json para as regras de precedência. Quando esse arquivo está no host do runner, o Claude Code ignora os servidores MCP que o plano de controle da Anthropic entrega a uma sessão, incluindo conectores do claude.ai, e os nomeia em um aviso no stderr do processo filho da sessão, que o runner registra no nível de logdebug. Antes da v2.1.229, essas sessões encerravam na inicialização comYou cannot dynamically configure MCP servers when an enterprise MCP config is present. - A chave
managedMcpServersnas configurações gerenciadas no host do runner: fornece servidores HTTP e SSE sem assumir controle exclusivo, de modo que os servidores das outras fontes ainda são carregados. Requer o Claude Code v2.1.259 ou posterior. <repo>/.mcp.json: escopo de projeto. Fora de uma thread de projeto, faça commit do arquivo no repositório, e seus servidores são aprovados automaticamente em sessões na nuvem. Em uma sessão com vários repositórios, no máximo o arquivo de um repositório é carregado.- Um arquivo
--mcp-configdo seu script wrapper: acrescente a flag depois de"$@", como emexec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --mcp-config /etc/claude/mcp.json. Para manter os servidores desse arquivo em execução quando os servidores entregues à sessão forem atualizados no meio da sessão, faça com que seu wrapper passe o ambiente completo para o processo filho, como oexecfaz. Antes da v2.1.299, os servidores stdio, HTTP e SSE desse arquivo eram encerrados na primeira atualização desse tipo que não os nomeasse.
Quando a entrega de conectores está habilitada para sua organização, o plano de controle da Anthropic entrega os conectores que você configurou no claude.ai para sessões criadas interativamente por meio de configuração MCP fornecida pelo servidor, roteada através de api.anthropic.com. Sessões criadas programaticamente, como despachos pela CLI, não recebem a entrega de conectores; forneça servidores MCP a elas por meio de qualquer uma das outras fontes listadas nesta seção. O token OAuth do processo filho não carrega um escopo para buscar conectores diretamente, então o processo filho não tenta fazer essa busca por conta própria; a entrega é conduzida pelo servidor.
[!NOTE]
Em uma sessão que recebeu um conector, um servidor com o mesmo nome vindo do
.claude.jsondo host ou do.mcp.jsonde um repositório não é conectado. Para usar ambos, dê um nome diferente ao seu servidor.
O settings.json não contém definições de servidores MCP, e não há um campo mcpServers de nível superior no esquema de configurações. Nas configurações gerenciadas, forneça servidores com a chave managedMcpServers.
As sessões herdam o ambiente do runner, então defina ENABLE_TOOL_SEARCH lá para controlar a busca de ferramentas MCP em todas as sessões que um runner inicia; a página de MCP aborda os valores.
Aguardar os servidores MCP antes do primeiro turno
Uma sessão auto-hospedada aguarda brevemente pelos servidores MCP que ainda estão se conectando, em dois pontos distintos. Um servidor que perde uma espera fica com suas ferramentas ausentes quando o primeiro turno começa, e elas ficam disponíveis mais tarde sem nenhuma ação da sua parte. As duas esperas são:
- Inicialização da sessão: antes de a lista de ferramentas ser obtida pela primeira vez, a sessão aguarda até 5 segundos por padrão por um servidor HTTP ou SSE cuja entrada define
alwaysLoad: true, ou por todos os servidores quando você defineMCP_CONNECTION_NONBLOCKING=0no ambiente do runner. Caso contrário, os servidores HTTP e SSE se conectam em segundo plano. Enquanto a sessão aguarda aqui, ela demora mais para inicializar.MCP_CONNECT_TIMEOUT_MSaltera o padrão de 5 segundos. - Primeiro turno: depois que a mensagem chega, o primeiro turno aguarda até 2 segundos pelos servidores stdio que ainda estão se conectando. Enquanto a sessão aguarda aqui, a primeira resposta demora mais. Para alterar a duração dessa espera, defina
CLAUDE_CODE_MCP_STARTUP_WAIT_MSno ambiente do runner. Isso não altera quais servidores a espera abrange. Requer o Claude Code v2.1.274 ou posterior.
O claude mcp add não tem uma flag alwaysLoad. Para definir a chave, adicione o servidor com claude mcp add-json, que a recebe no JSON do servidor e a grava em .claude.json. No seu Dockerfile:
RUN claude mcp add-json core '{"type":"http","url":"https://mcp.example.com/mcp","alwaysLoad":true}' --scope user
Se as ferramentas de um servidor também não aparecerem nos turnos seguintes, verifique se o servidor chegou à sessão, conforme descrito em Servidores MCP.
Desativar as ferramentas de sessão integradas
O plano de controle da Anthropic anexa seu próprio servidor MCP, chamado Claude Code Remote, às sessões na nuvem. O Claude usa as ferramentas do servidor para criar tarefas agendadas na nuvem, iniciar e direcionar outras sessões na nuvem, anexar mais repositórios e acompanhar a atividade de pull requests.
Para desativar o servidor inteiro, adicione uma regra de negação no nível do servidor às suas configurações. O plano de controle registra o servidor sob um de três nomes, dependendo de como a sessão foi criada. O Claude Code corresponde ao nome em uma regra de forma exata, incluindo maiúsculas e minúsculas, então escreva a regra uma vez para cada nome, como mostrado:
{
"permissions": {
"deny": [
"mcp__Claude_Code_Remote",
"mcp__claude-code-remote",
"mcp__bf7c680d-5fdc-5ef4-b4a0-abadb619bf0a"
]
}
}
Uma regra que nomeia o servidor inteiro também cobre ferramentas que o servidor ganhar posteriormente. Para desativar uma ferramenta e manter as demais, acrescente mais dois sublinhados e o nome da ferramenta a cada regra, como em mcp__Claude_Code_Remote__add_repo. Para impedir que o servidor se conecte, em vez de remover suas ferramentas, adicione os três nomes sem o prefixo mcp__ como entradas serverName em deniedMcpServers.
Coloque as regras nas configurações gerenciadas pelo servidor para alcançar as sessões sem alterar o runner, ou em ~/.claude/settings.json no runner. Em um runner que envia requisições de modelo para o Bedrock ou o Agent Platform, use esse arquivo, porque as configurações gerenciadas pelo servidor não alcançam essas sessões. Permissões e aprovação de ferramentas explica como as configurações no runner chegam às sessões.
Para confirmar que as regras entraram em vigor, inicie uma sessão no ambiente e peça ao Claude para listar suas ferramentas MCP. O Claude Code remove uma ferramenta negada do contexto do Claude, então as ferramentas negadas ficam ausentes da resposta dele.
Prompt sessions to push their work
Sessões hospedadas pela Anthropic executam um hook Stop, o hook Claude Code que é executado quando Claude termina de responder, que solicita a Claude fazer commit e push de seu trabalho. O runner não instala um. Sem ele, uma sessão que termina com mudanças não confirmadas deixa esse trabalho apenas no disco do runner, e o botão Create PR em claude.ai/code fica inativo até o branch existir no remoto.
A implementação de referência abaixo tem duas partes. Mescle o bloco de configurações em ~/.claude/settings.json no host do runner, que o runner semeia em cada sessão, e salve o script como ~/.claude/hooks/stop-hook-nudge.sh no host do runner e torne-o executável:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"timeout": 10,
"command": "\"$CLAUDE_CONFIG_DIR/hooks/stop-hook-nudge.sh\""
}
]
}
]
}
}
#!/bin/sh
# Stop-hook reference implementation for self-hosted runners.
#
# Nudges Claude once per turn if the project directory has uncommitted
# changes OR unpushed commits, so work isn't lost when an idle session
# is released and so the "Create PR" button on claude.ai/code lights up.
#
# Runner-level (no repo changes): drop this file at ~/.claude/hooks/ on
# the runner host and merge the accompanying Stop-hook settings block
# into ~/.claude/settings.json — the runner seeds both into every session.
# Repo-level alternative: commit to <repo>/.claude/hooks/ and change the
# settings.json command path to $CLAUDE_PROJECT_DIR/.claude/hooks/.
#
# stdin: hook JSON payload (see https://code.claude.com/docs/en/hooks)
# stdout: {"decision":"block","reason":"..."} to nudge, or nothing to allow stop.
# Re-entry guard: the harness sets stop_hook_active=true when re-invoking
# the Stop hook after a block. Bail so we only nudge once per turn. The
# harness emits compact JSON (no space after the colon), which this
# pattern relies on; use jq if you need a whitespace-tolerant check.
in=$(cat)
case "$in" in *'"stop_hook_active":true'*) exit 0 ;; esac
d="$CLAUDE_PROJECT_DIR"
# Hardening: this script runs outside Claude Code's Bash sandbox, so its git
# calls skip some features that can start a program named in the checkout's
# git settings. They don't skip them all, so don't count on the sandbox alone.
GIT_OPTIONAL_LOCKS=0
export GIT_OPTIONAL_LOCKS
git() { command git -c core.fsmonitor= -c core.hooksPath=/dev/null "$@"; }
# Not a git repo → nothing to nudge.
git -C "$d" rev-parse --git-dir >/dev/null 2>&1 || exit 0
# No remote → "push to the remote" is unsatisfiable; bail.
[ -z "$(git -C "$d" remote 2>/dev/null)" ] && exit 0
# Uncommitted changes (staged, unstaged, or untracked). Exclude .claude/
# entirely — operator-seeded settings and CLI-written runtime state
# (scheduler lock, worktrees, routine state) live there and neither is
# "uncommitted work" the model needs to 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
# Unpushed commits. Count commits on HEAD not reachable from any
# remote-tracking ref or FETCH_HEAD. This works uniformly for:
# - init+fetch checkouts (runner default: only FETCH_HEAD exists)
# - clone-based checkouts (origin/* exist)
# - the runner default: the child starts on the session's outcome
# branch, which the runner creates after checkout
# - detached HEAD, when a custom setup skips that branch creation
# With no reference point at all (never fetched), stay silent rather
# than false-positive on a read-only turn.
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 is either "" or "FETCH_HEAD", intentional word-split
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 is attacker-influenced — git-check-ref-format(1) allows `"`
# in ref names. `\` is forbidden (rule 10) but escaped anyway as cheap
# defense-in-depth.
# Escape JSON metacharacters before interpolating into the hand-built
# payload so a branch like x","continue":false can't inject keys into
# the hook-output JSON the harness parses. $unpushed is safe — the
# -gt guard above rejects anything that isn't a plain integer.
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
O hook solicita a Claude fazer commit e push antes da sessão terminar, e fica silencioso quando o diretório não é um repositório git ou não tem remoto. Para uma sessão com vários repositórios, consulte o que $CLAUDE_PROJECT_DIR nomeia.
O hook Stop apenas solicita a Claude. Para que o commit e o push funcionem, configure:
- Uma identidade git: defina
user.nameeuser.emailna imagem do seu runner, ou inicie o runner com--configure-git - Credenciais de push: use o git gerenciado pela Anthropic, ou forneça credenciais conforme descrito em Incluir a configuração do git na sua imagem
- Para sessões retomadas: defina
--push-outcome-on-releaseem todos os runners do ambiente. Uma sessão retomada então começa a partir dos commits que Claude enviou por push para o branch da sessão. Para os limites da flag, consulte Sessões retomadas perdem trabalho sem push.
O hook Stop solicita no máximo uma vez por turno. Se o commit ou o push falhar, o turno termina mesmo assim. Procure o erro do git na sessão e corrija o que ele indica.
Em um checkout grande, aumente o timeout nas configurações do hook Stop e reinicie os runners. O git status do script ignora core.fsmonitor, então pode levar mais do que os 10 segundos definidos ali. Um hook que atinge o timeout encerra o turno sem nenhuma solicitação.
Submódulos e o hook Stop
O hook Stop verifica em qual commit cada submódulo está, não os arquivos dentro dele:
- Um novo commit em um submódulo: Claude recebe a solicitação até que o repositório pai o registre, mesmo que
.gitmodulesdefinaignore = allpara esse submódulo - Arquivos sem commit em um submódulo: Claude não recebe a solicitação, então esse trabalho permanece apenas no disco do runner
- Commits sem push em um submódulo: o hook Stop não verifica se eles foram enviados por push, então o repositório pai pode apontar para um commit que ninguém mais consegue buscar
Substituir uma cópia anterior do script do hook Stop
Procure GIT_OPTIONAL_LOCKS na sua cópia de stop-hook-nudge.sh. Se não estiver presente, você tem uma cópia anterior. Substitua-a pelo script atual onde quer que você o tenha instalado e, em seguida, reinicie os runners. Mantenha suas configurações do hook Stop, exceto para aumentar o timeout em um checkout grande.
- Quando: se suas sessões são executadas com o sandbox do Bash ativado, substitua-a agora. Caso contrário, substitua-a no seu próximo build de imagem ou reinicialização do runner.
- O que muda: arquivos sem commit em um submódulo não solicitam mais a Claude. Um novo commit em um submódulo que
.gitmodulesmarca comoignore = allagora solicita.
[!WARNING]
Os hooks são executados fora do sandbox do Bash, então as chamadas git do script atual ignoram alguns recursos do git que podem iniciar um programa nomeado nas configurações git do checkout. Elas não ignoram todos. Não confie apenas no sandbox e isole as sessões conforme descrito em Proteger sua implantação.
Permissions and tool approval
Uma sessão auto-hospedada não tem terminal anexado, então um prompt de permissão não respondido paralisa a volta até o usuário responder na UI. O plano de controle da Anthropic envia a lista de ferramentas de cada sessão e regras de permissão com a carga de trabalho; a configuração padrão pré-aprova chamadas de ferramentas rotineiras, incluindo Bash, e sessões na nuvem pré-aprovam edições de arquivo independentemente do modo. Uma chamada que nada pré-aprova solicita através da UI da sessão.
[!NOTE]
Apenas fixe auto mode em um ambiente cujos contêineres de sessão são executados com default-deny network egress e o resto da seção de hardening em vigor. Chamadas de ferramentas rotineiras, incluindo solicitações de rede
Bash, são executadas sem um humano no loop tanto no conjunto de ferramentas pré-aprovadas padrão quanto em auto mode, então o limite de rede é o que limita para onde essas chamadas podem alcançar.
Para manter prompts ao mínimo independentemente do que o plano de controle envia, fixe auto mode de seu script wrapper ou hook command. Auto mode permite que sessões sejam executadas sem prompts de permissão rotineiros: um modelo classificador separado revisa ações antes de serem executadas e bloqueia as que rejeita, e regras de ask explícitas ainda forçam um prompt; a página de modos de permissão cobre o que o classificador verifica. O runner anexa flags computadas pelo servidor antes de invocar o wrapper, e para flags de valor único como --permission-mode o parser honra a última ocorrência, então uma flag que você anexa após "$@" substitui o valor enviado pelo servidor:
#!/bin/bash
exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --permission-mode auto
Para pré-aprovar ferramentas específicas em vez disso, anexe --allowed-tools com suas regras, por exemplo --allowed-tools "Bash(bazel *) Bash(yarn *) mcp__internal__*". Flags de lista como --allowed-tools e --disallowed-tools acumulam em ocorrências em vez de substituir, então suas regras se aplicam em cima de quaisquer regras que o plano de controle envia. Para estreitar, anexe --disallowed-tools, que nega ferramentas mesmo se outra regra as permite.
How each session's config is assembled
O runner dá a cada sessão seu próprio diretório de configuração, semeado de um snapshot do ~/.claude/ do host que o runner captura uma vez na inicialização: settings.json, CLAUDE.md, hooks, agentes, comandos e skills em sua imagem do runner se aplicam a cada sessão como a linha de base de nível de usuário. Se você alterar a configuração em um host em execução, a alteração tem efeito apenas após reiniciar o runner.
Defina SELF_HOSTED_RUNNER_HOST_CONFIG_DIR para semear de um caminho diferente, ou aponte-o para um diretório vazio para desabilitar a semeadura.
As sessões também leem estes arquivos de configurações:
- Configurações de projeto: fora de uma thread de projeto, um
.claude/settings.jsoncom commit no repositório se sobrepõe à linha de base de nível de usuário. Em uma sessão com vários repositórios, no máximo o arquivo de um repositório tem efeito. - Configurações gerenciadas: as sessões leem
managed-settings.jsondo caminho de sistema padrão em sua imagem do runner. Para saber se suas chaves se aplicam ao lado de server-managed settings, consulte como Claude Code combina fontes gerenciadas.
Para a ordem em que essas fontes se aplicam, consulte settings precedence.
Quando o plano de controle da Anthropic fornece a uma sessão hooks do Claude Code, o runner os instala ao lado, e não por cima, da sua própria configuração. Requer Claude Code v2.1.229 ou posterior; runners mais antigos ignoram os hooks fornecidos. Os hooks fornecidos executam python3, então instale python3 na sua imagem do runner.
- Onde eles pousam: o runner escreve cada script de hook fornecido para um subdiretório reservado
hooks/.ccr-launcher/do diretório de configuração da sessão e registra os scripts em um arquivo de configurações separado que passa para a sessão com--settings, deixando osettings.jsonsemeado e seus próprios scripts emhooks/<name>intocados. O runner recria o subdiretório reservado para cada sessão e não semeia conteúdo do host em~/.claude/hooks/.ccr-launcher/em sessões. - Quem os autora: o plano de controle popula os scripts de constantes fixas em sua própria implantação, nunca de entrada por sessão ou de terceiros.
- O que ainda os governa: hooks entregues através de
--settingsentram na configuração de hook mesclada ordinária, não na camada gerenciada, então suas configurações gerenciadas ainda se aplicam.disableAllHooksos desabilita, e eles não estão entre as categorias queallowManagedHooksOnlymantém carregadas.
Quando uma pessoa inicia sua própria sessão, o Claude Code baixa as skills habilitadas para sua conta claude.ai no diretório de configuração dessa sessão. Uma execução de tarefa agendada na nuvem não recebe as skills de seu proprietário, e uma sessão que envia requisições de modelo para Bedrock ou Agent Platform não baixa nenhuma. Para uma skill de que essas sessões precisem, adicione-a à sua imagem do runner ou, fora de uma thread de projeto, faça commit dela no .claude/skills/ do repositório.
Fora das sessões do Claude Tag, uma sessão em um ambiente auto-hospedado é executada com a memória automática desativada por padrão. Para instruções que devem persistir entre sessões, use o CLAUDE.md em sua imagem do runner ou no repositório.
O snapshot do ~/.claude/ do host feito pelo runner deixa de fora o diretório projects/. O local de armazenamento padrão da memória automática fica sob esse diretório. Se você colocar arquivos de memória lá, o runner não os semeia nas sessões, e eles não ativam a memória automática.
Stop sessions that fail to fetch server-managed settings
Para impedir que uma sessão que busca configurações gerenciadas pelo servidor inicie quando essa busca falha, defina forceRemoteSettingsRefresh como true no managed-settings.json em sua imagem do runner. Uma resposta da API da Anthropic informando que sua organização não tem configurações gerenciadas pelo servidor conta como uma busca bem-sucedida.
Antes de adicionar a chave, verifique se o ambiente executa sessões de canal do Claude Tag ou envia requisições de modelo para Bedrock ou Agent Platform:
- Sessões de canal do Claude Tag: deixe a chave de fora nos runners de um ambiente para o qual um Owner direciona canais do Claude Tag. A API da Anthropic recusa a requisição dessas sessões por configurações gerenciadas pelo servidor, então, com a chave definida, elas não conseguem iniciar. Coloque as regras delas no
managed-settings.jsonda imagem do runner, que elas ainda leem. Para usar a chave em suas outras sessões, execute-as em um ambiente separado. - Bedrock ou Agent Platform: sessões nesses provedores não buscam configurações gerenciadas pelo servidor, então elas iniciam quer você defina a chave ou não. Em vez disso, aplique suas regras a partir do mesmo
managed-settings.json.
Este managed-settings.json define a chave:
{
"forceRemoteSettingsRefresh": true
}
Defina a chave nesse arquivo mesmo que você também a defina no console de administração do claude.ai. Uma nova sessão não tem uma cópia em cache de suas configurações gerenciadas pelo servidor, então, se você definir a chave apenas no console, a sessão ainda não a tem. Se a busca dela falhar nesse momento, a sessão é executada sem essas configurações até que uma busca posterior seja bem-sucedida.
Para confirmar que uma busca de configurações com falha é o motivo pelo qual uma sessão não iniciou, verifique a última saída do Claude Code na sessão ou o log do runner. Ela inclui Your organization requires remote managed settings to load, but they could not be loaded.
Repository settings in sessions with several repositories
Em uma thread de um projeto, nenhum .claude/settings.json de repositório tem efeito como configurações de projeto e nenhum .mcp.json ou skill de repositório é carregado, onde quer que a thread inicie. Para fornecer uma skill a essas threads, adicione-a à sua imagem do runner.
Em qualquer outra sessão com vários repositórios, o Claude Code lê as configurações de projeto do diretório em que a sessão inicia, então no máximo o .claude/settings.json de um repositório tem efeito como configurações de projeto. Um hook definido no arquivo de outro repositório não é executado, uma regra deny nele não se aplica, e seu env não é definido.
--capacity 1, o padrão, com o checkout integrado: a sessão inicia no primeiro repositório de sua lista de repositórios. O.claude/settings.jsondesse repositório tem efeito como configurações de projeto e seu.mcp.jsoné carregado, e os dos outros repositórios não.- Um
--capacityacima de um, ou um hookcheckout: a sessão inicia em um diretório por sessão que contém os checkouts. Nenhum.claude/settings.jsonde repositório tem efeito como configurações de projeto, nenhum.mcp.jsonde repositório é carregado, e$CLAUDE_PROJECT_DIRem um comando de hook é esse diretório, não um checkout.
O CLAUDE.md de cada repositório é carregado onde quer que a sessão inicie. Fora de uma thread de projeto, suas skills também são carregadas. O runner passa cada repositório para o Claude Code como um diretório adicional, então o Claude Code também lê as chaves enabledPlugins e extraKnownMarketplaces do .claude/settings.json de cada repositório.
Para executar um hook ou aplicar uma regra de permissão em todas as sessões, coloque-o em ~/.claude/settings.json no host do runner. O runner semeia o arquivo do host em cada sessão, onde quer que a sessão inicie. Escreva um caminho em uma regra Read ou Edit como um padrão absoluto // ou relativo ao diretório home ~/, porque outros padrões se ancoram na origem das configurações ou no diretório atual.
Repository-committed permission rules
Não coloque uma entrada "Edit", "Write" ou "NotebookEdit" nua em um permissions.allow confirmado no repositório. Uma regra de ferramenta de arquivo nua corresponde à ferramenta independentemente do caminho, concedendo escritas em qualquer lugar no host em vez de apenas o workspace, então a guarda de confinamento de escopo de escrita do runner sinaliza a sessão; com --confine-repo-settings enforce ela recusa gerar a sessão em vez de registrar e continuar. Consulte a seção de hardening.
Um repositório não precisa de nenhuma regra de ferramenta de arquivo: sessões na nuvem pré-aprovam edições de arquivo independentemente do modo. Se você fizer uma regra, escope-a para o workspace, como "Edit(/**)"; uma barra inicial única é relativa à raiz do projeto, que é o workspace da sessão. Regras de ferramenta de arquivo nuas são aceitáveis no settings.json de nível de host do operador, já que esse arquivo não é confirmado no repositório.
Um defaultMode de auto é apenas honrado do arquivo de configurações de nível de imagem ou de nível de usuário, então um repositório verificado não pode se conceder auto mode. Para quais modos sessões na nuvem aceitam e a sintaxe de regra completa, consulte permission modes.
What's next
- Reference: cada flag CLI, variável de ambiente e métrica
- Verify session identity: valide o token de sessão de serviços fora do runner