Hooks do Claude Code: os 3 bugs que a v2.1.141 eliminou

Guia prático para atualizar hooks do Claude Code em produção: configure terminalSequence, args:string[] e continueOnBlock sem gambiarras em shell.

Saturday, September 5, 2026Omid Saffari
Hooks do Claude Code: os 3 bugs que a v2.1.141 eliminou

Três dos bugs de hooks do Claude Code que mais me atrapalharam no primeiro trimestre foram corrigidos discretamente em apenas 7 dias. Na manhã seguinte, apaguei três soluções provisórias em shell do meu repositório administrativo.

A semana em que três bugs reais desapareceram

Entre 6 e 13 de maio de 2026, a Anthropic lançou as versões 2.1.132 a 2.1.141 do Claude Code. A maior parte das mudanças se concentrou no sistema de hooks.

Eu percebi porque havia três remendos no meu repositório com comentários // TODO: remove when claude-code fixes this; em menos de uma semana, consegui apagar todos eles.

Estes eram os bugs, em ordem de prejuízo:

O primeiro fazia notificações do desktop sumirem. Meu hook Notification enviava o alerta sonoro do sistema operacional para stdout, para que eu fosse avisado quando uma compactação demorada terminasse. Funcionava enquanto o Claude Code controlava o TTY em primeiro plano, mas falhava silenciosamente em qualquer outro cenário: divisões do tmux, terminais integrados do VS Code e painéis do WezTerm quando o foco estava no painel ao lado. A correção chegou como terminalSequence na versão 2.1.141.

O segundo era o descontrole do escape de caracteres no shell. Meu hook Stop executava bash -lc "node post-stop.js --reason '$CLAUDE_STOP_REASON'", e bastava o motivo retornado pelo Claude conter um apóstrofo para tudo quebrar. A solução foi a forma de execução args: string[], incluída na 2.1.139.

O terceiro fazia rejeições de PostToolUse encerrarem o turno, em vez de devolverem o controle ao Claude. Precisei desativar por completo um hook de validação de schema porque o runtime tratava uma decisão block como erro fatal. A 2.1.139 trouxe continueOnBlock, que transforma o bloqueio em um sinal de nova tentativa e injeta o motivo no contexto.

O settings.json completo está no final. Sem enrolação no meio do caminho.

Como funcionam os hooks do Claude Code em 90 segundos

O Claude Code 2.1.x oferece nove eventos de hook: SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact e SessionEnd. Cada hook é um comando iniciado pelo runtime com um ambiente documentado e um payload recebido por stdin. O runtime interpreta o stdout do hook como JSON, e alguns campos específicos controlam seu comportamento: decision (allow/deny/block), reason (string devolvida ao Claude ou exibida ao usuário), terminalSequence (bytes brutos enviados ao TTY de controle) e alguns campos próprios de cada evento.

O conceito essencial é simples: para o runtime, o stdout do hook é a fonte de verdade. Um campo que ele não interpreta é peso morto. Um campo que ele interpreta muda o que o Claude verá no turno seguinte. É disso que todo o mecanismo depende.

terminalSequence: notificações no desktop sem controlar o TTY

Antes da 2.1.141, meu hook Notification era assim:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "bash -lc 'printf \"\\a\" && notify-send \"Claude\" \"$CLAUDE_MESSAGE\"'"
    }]
  }
}

O printf "\a" deveria acionar o alerta sonoro do terminal. Na prática, ele escrevia \a no descritor de arquivo herdado pelo processo do hook, que não corresponde ao terminal do usuário quando o Claude Code não está em primeiro plano. Se o Claude estava no painel 2 de uma divisão do tmux e eu editava no painel 1, o alerta nunca chegava ao terminal externo. O notify-send funcionava, mas a notificação ficava na bandeja do GNOME e eu acabava não vendo.

A versão 2.1.141 acrescentou o campo terminalSequence ao JSON do stdout do hook. O runtime pega essa string e a escreve diretamente no dispositivo de terminal de controle, sem passar pelo stdio do processo do hook. Depois da atualização, a configuração ficou assim:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "node hooks/notify.mjs"
    }]
  }
}
JavaScript
import { execSync } from "node:child_process";

const payload = JSON.parse(
  await new Response(process.stdin).text()
);

execSync(`notify-send "Claude" ${JSON.stringify(payload.message)}`);

process.stdout.write(JSON.stringify({
  terminalSequence: ""
}));

 é BEL. O runtime o escreve no TTY de controle, e o terminal externo toca mesmo quando o Claude está em um painel em segundo plano. macOS Terminal, iTerm2, WezTerm e Alacritty se comportaram corretamente nos meus testes. O tmux repassa BEL; de qualquer forma, mantenha set -g allow-passthrough on no seu tmux.conf por causa do OSC 52.

Há um caso de borda importante: por padrão, o terminal integrado do VS Code ignora BEL. Ative "terminal.integrated.enableBell": true nas configurações do usuário para receber o alerta também ali.

args: string[] eliminou o escape de shell nos meus hooks

O hook Stop é disparado quando um turno termina. Eu o uso para registrar os metadados da sessão em uma instância do Cloudflare D1 e, assim, poder pesquisar execuções antigas com grep.

Antes da 2.1.139, eu usava esta configuração:

JSON
{
  "hooks": {
    "Stop": [{
      "command": "bash -lc \"node scripts/post-stop.js --session $CLAUDE_SESSION_ID --reason '$CLAUDE_STOP_REASON'\""
    }]
  }
}

No primeiro mês, esbarrei em três bugs de escape com esse formato:

Apóstrofos em $CLAUDE_STOP_REASON fechavam o argumento entre aspas simples, e o restante passava a ser interpretado como tokens do shell. Quando o Claude retornava um motivo como user's request completed, o hook falhava e o registro da sessão se perdia.

Backticks nos nomes das ferramentas. Quando um hook era disparado com $CLAUDE_TOOL_NAME contendo a string `bash` porque o Claude a havia usado em uma resposta, o shell tentava executar o conteúdo entre backticks como um subshell. Inofensivo nesse caso, assustador em qualquer outro.

Unicode nos prompts dos usuários. Na maioria das vezes, o UTF-8 atravessa bash -lc sem problemas, mas determinadas combinações de pontos de código CJK e configurações de locale descartavam bytes silenciosamente.

A versão 2.1.139 introduziu comandos no formato exec. Ao passar args como um array de strings, o runtime inicia o comando diretamente, sem colocar um shell no meio:

JSON
{
  "hooks": {
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }]
  }
}

O runtime resolve as variáveis $CLAUDE_* do próprio ambiente antes de chamar execve. Não há shell, interpolação do shell nem preocupação com aspas. A string user's request completed chega ao meu script Node como uma única entrada argv[5], exatamente como o Claude a produziu.

Em um hook PreToolUse executado a cada chamada de ferramenta, isso faz diferença.

Quando manter o formato de shell: sempre que o comando precisar de pipes, redirecionamentos ou expansão de curingas. args:[] e command:"" são mutuamente exclusivos em uma mesma entrada de hook. Portanto, se você precisa de node x.js | jq | tee log, continue com command:"" e aceite o custo do escape. Para 90% dos hooks, o formato exec é a escolha certa.

continueOnBlock: uma rejeição de PostToolUse que realmente tenta de novo

Essa foi a correção pela qual esperei mais tempo. Uso um hook PostToolUse para validar a saída de toda chamada de ferramenta que grava no disco: se o Claude escreve um arquivo TypeScript, o hook executa tsc --noEmit e rejeita o resultado quando há erros de tipo.

Antes da 2.1.139, o fluxo de rejeição não funcionava:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "command": "node hooks/validate-ts.mjs"
    }]
  }
}

Quando validate-ts.mjs retornava { "decision": "block", "reason": "tsc failed: ..." }, o runtime encerrava o turno. O Claude não recebia o motivo. O usuário via apenas a mensagem enigmática "hook blocked the operation" e precisava enviar outro prompt manualmente, colando o erro. Depois que isso interrompeu turnos produtivos em três sessões reais, desativei o hook.

A versão 2.1.139 adicionou continueOnBlock como configuração individual de cada hook:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }]
  }
}

Agora, uma decisão block devolve a string reason ao contexto do Claude como um erro de resultado da ferramenta. O Claude recebe tsc failed: src/api.ts(14,3): error TS2322: Type 'string' is not assignable to type 'number' e se corrige no turno seguinte. O usuário não vê nada, porque o loop é interno.

maxAttempts define um limite rígido. Sem ele, uma validação não determinística — por exemplo, uma que dependa de uma API remota temporariamente indisponível — consome contexto em novas tentativas infinitas. Eu uso três. Depois de três falhas, o hook escala para um bloqueio definitivo e mostra o erro ao usuário.

Antipadrão: não ative continueOnBlock em hooks cujas decisões dependam do horário. Um hook que rejeita gravações durante uma janela de deploy continuará em loop se o deploy ainda estiver acontecendo quando o Claude tentar de novo. Nesse caso, condicione a execução a $CLAUDE_EFFORT ou inclua um contador de tentativas no script do hook.

A dupla extra: $CLAUDE_EFFORT e CLAUDE_PROJECT_DIR

Duas novas variáveis de ambiente apareceram discretamente nesse intervalo, e ambas são bastante úteis.

A versão 2.1.133 passou a injetar $CLAUDE_EFFORT no ambiente dos hooks. Os valores são low, medium, high e xhigh, de acordo com o nível de esforço do Claude no turno atual. Assim, um hook pode variar o comportamento conforme o esforço sem precisar interpretar o prompt:

JavaScript
const effort = process.env.CLAUDE_EFFORT;

if (effort === "low" || effort === "medium") {
  // skip expensive tsc check on quick edits
  process.stdout.write(JSON.stringify({ decision: "allow" }));
  process.exit(0);
}

// run full tsc --noEmit on high/xhigh

Economizo cerca de 800ms por edição rápida ao pular o tsc em níveis baixos de esforço. Em um turno de planejamento high, no qual o Claude escreve uma dúzia de arquivos, a verificação completa ainda é executada e encontra bugs reais.

A versão 2.1.139 incluiu CLAUDE_PROJECT_DIR no ambiente dos servidores MCP stdio iniciados pelo runtime. Antes disso, esses servidores precisavam inferir a raiz do workspace por meio de process.cwd(), o que falhava quando o usuário abria o Claude Code a partir de um subdiretório. Agora, qualquer servidor MCP pode ler process.env.CLAUDE_PROJECT_DIR e resolver corretamente caminhos relativos ao workspace.

Quem mantém um servidor MCP deve atualizar a resolução de caminhos do manifesto para usar CLAUDE_PROJECT_DIR, recorrendo a cwd() como fallback para clientes antigos. São duas linhas de mudança que eliminam toda uma classe de bugs.

O settings.json exato que uso em produção

Este é o bloco de produção do omidsaffari-admin, com alguns dados omitidos. Seis DOs e um Workflow dependem da saída dos hooks para notificações no desktop e validações de CI.

JSON
{
  "model": "claude-sonnet-4-7-20260501",
  "permissions": {
    "edit": "ask"
  },
  "hooks": {
    "SessionStart": [{
      "args": ["node", "hooks/session-start.mjs"]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "args": ["node", "hooks/gate-bash.mjs"]
    }],
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }],
    "Notification": [{
      "args": ["node", "hooks/notify.mjs"]
    }],
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }],
    "PreCompact": [{
      "args": ["node", "hooks/notify.mjs"]
    }]
  }
}
JSON
{
  "devDependencies": {
    "@anthropic-ai/claude-code": "2.1.141"
  }
}

Instale e verifique:

Bash
pnpm add -D @anthropic-ai/claude-code@2.1.141
claude --version    # expect: 2.1.141
claude config doctor    # expect: 0 hook warnings

Os destaques: continueOnBlock e maxAttempts formam a dupla do loop de novas tentativas introduzida na 2.1.139. O formato args:[] aparece em todo hook que não precisa de recursos do shell — nesta configuração, são todos eles. Os hooks Notification e PreCompact compartilham o notify.mjs porque ambos precisam de notificações no desktop com a mesma saída terminalSequence.

Checklist de implantação e quando não atualizar

Antes de começar, faça um snapshot do settings.json atual. Anote quais hooks usam command:"" e quais já adotam args:[]. Liste todos os tipos de evento configurados.

Migre um tipo de evento por vez e deixe cada migração rodar por 24 horas. Acompanhe os avisos de hooks com claude config doctor e procure as strings decision e reason nos logs para confirmar que o runtime está recebendo o esperado.

Esta é a ordem que eu seguiria:

  1. Atualize o pacote para a versão 2.1.141.
  2. Converta um hook de command:"" para args:[] e confirme que ele é disparado.
  3. Adicione terminalSequence ao hook Notification e teste o alerta em um painel do tmux em segundo plano.
  4. Adicione continueOnBlock ao hook PostToolUse que mais incomoda. Acompanhe uma sessão real para confirmar que o Claude recebe o motivo e se corrige.
  5. Migre o restante dos hooks para args:[] em lotes.

Não atualize se você usa uma instalação gerenciada, presa à versão definida por um SDK pai que ainda não validou a 2.1.141, ou se depende de um campo de hook que a 2.1.141 tornou obsoleto. Até o momento desta publicação, nenhuma mudança do intervalo de maio quebrou campos existentes. Mesmo assim, fixe a versão e teste em uma branch antes de liberar para a equipe.

Para ter o playbook completo de fixação de versões, estratégia de hooks e estruturação de projetos que uso com seis agentes em produção, consulte o Checklist de configuração do Claude Code + Codex. Ele reúne os mesmos padrões de settings.json e também a estrutura do lado dos agentes — Workflows, DOs e bindings do Vectorize — que esses hooks controlam.

Para conferir uma análise complementar sobre a execução desses agentes em sandboxes remotos, leia o comparativo entre ambientes do Cursor Cloud Agent e Cloudflare Workers. Para entender o contexto da stack de produção por trás deste settings.json, veja o artigo sobre o engenheiro 100x com Cloudflare.

terminalSequence funciona no tmux?

Sim, com uma ressalva. O runtime escreve a sequência no dispositivo de terminal de controle, que o tmux encaminha ao terminal externo quando set -g allow-passthrough on está presente no tmux.conf ou quando a sequência é um BEL simples (). BEL sempre passa; sequências OSC exigem que o passthrough esteja ativado.

Posso usar args:[] e command:'' juntos na mesma configuração de hook?

Não. Eles são mutuamente exclusivos em cada entrada de hook. Use o formato exec (args:[]) para comandos iniciados diretamente e o formato shell (command:"") quando precisar de pipes, redirecionamentos ou expansão de curingas. Se for necessário combinar os dois, crie um script wrapper que use o formato exec e concentre internamente os recursos de shell.

continueOnBlock pode entrar em loop infinito se o Claude repetir a mesma falha de validação?

Não, desde que maxAttempts esteja definido. O runtime limita as novas tentativas a esse número e depois escala para um bloqueio definitivo. Sem maxAttempts, é possível consumir contexto indefinidamente com um validador não determinístico. Sempre defina esse valor. Três é um padrão razoável para validações de tipo; um é o número certo para qualquer processo que dependa de estado remoto.

$CLAUDE_EFFORT está disponível em todos os eventos de hook?

Sim. Desde a 2.1.133, a variável é injetada no ambiente de todo hook iniciado pelo runtime. O valor reflete o nível de esforço do turno atual: SessionStart recebe o esforço selecionado pelo usuário ao iniciar, enquanto PostToolUse recebe aquele que estava ativo quando a ferramenta foi chamada.

O que deixa de funcionar se eu voltar para a 2.1.138?

terminalSequence, args:[], continueOnBlock e CLAUDE_PROJECT_DIR deixam de funcionar silenciosamente. O runtime ignora campos JSON desconhecidos e volta à interpretação de command:"". Seus hooks continuam sendo disparados, mas os novos comportamentos desaparecem. Teste o downgrade em uma branch antes de depender dele.

Última atualização
5 de set. de 2026
Categoria
Build

Prefira este site no Google

Adicionar omidsaffari.com como fonte preferida na Busca do Google

Marque omidsaffari.com como fonte preferida e o Google destaca o site para você em Top Stories, AI Overviews e AI Mode.

Artigos relacionados
Cursor preço: o Rollouts é grátis? Planos, créditos e custos

Cursor preço: o Rollouts é grátis? Planos, créditos e custos

Cursor Rollouts exige Teams ou Enterprise. Entenda os créditos de lançamento por 10 dias, cerca de 50 ou 500 alterações, e os custos a conferir.24 de set. de 2026Build
Unreal Agent na prática: como avaliar um agente de IA

Unreal Agent na prática: como avaliar um agente de IA

Veja como testar o Unreal Agent, um agente de IA para repositórios: isole o ambiente, salve a sessão em JSONL e meça custo, segurança e desempenho.24 de set. de 2026Build
Codex JetBrains com Air: do primeiro prompt à revisão

Codex JetBrains com Air: do primeiro prompt à revisão

Aprenda a instalar o Air Alpha, conectar o Codex ao JetBrains, fornecer o contexto certo e revisar a primeira alteração de código com segurança.23 de set. de 2026Build
JetBrains Air é grátis? Entenda quem paga cada camada

JetBrains Air é grátis? Entenda quem paga cada camada

JetBrains Air é grátis no plugin, mas agente, IDE e uso de API podem gerar custos. Veja as quatro formas de autorização e descubra qual conta paga.23 de set. de 2026Build
Firecrawl API com hospedagem própria: instalação e custos

Firecrawl API com hospedagem própria: instalação e custos

Entenda como instalar a Firecrawl API em infraestrutura própria, validar scrapes reais e comparar o custo operacional com o Firecrawl Cloud em 30 dias.22 de set. de 2026Build
Agentes de IA: quando um retry pago exige aprovação humana

Agentes de IA: quando um retry pago exige aprovação humana

Entenda por que retries pagos de agentes de IA precisam de aprovação humana no ponto da recompra, mesmo em fluxos que já mantêm pessoas no circuito.22 de set. de 2026Build
Automação com IA no MindStudio: preços, limites e quando vale a pena

Automação com IA no MindStudio: preços, limites e quando vale a pena

Veja como o MindStudio organiza automação com IA, quanto custam os planos e o uso de modelos e quais limites testar antes de adotar a plataforma.22 de set. de 2026Build
Wispr Flow ou Superwhisper: qual app de ditado compensa?

Wispr Flow ou Superwhisper: qual app de ditado compensa?

Compare Wispr Flow e Superwhisper em preço, privacidade, uso offline e recursos para equipes antes de escolher seu aplicativo de ditado por voz.22 de set. de 2026Build
Newsletter

Uma carta, todo domingo.Sistemas que funcionam, não hot takes.

Semanal. Sem spam. Cancele quando quiser.