Saltar para o conteúdo

Claude Code

Dentro dos hooks PEP: política deny-closed dentro do Claude Code

Por Olivares AI 9 min de leitura

Uma equipe de plataforma habilita Claude Code em toda a sua organização de engenharia. Para aplicar políticas sobre chamadas de ferramentas, eles conectam um servidor de hook — um processo local que recebe eventos de hook na entrada padrão (stdin) e retorna uma decisão em JSON na saída padrão (stdout). A primeira semana parece bem. PreToolUse é disparado antes de cada chamada de ferramenta, o hook retorna deny para qualquer coisa que toque produção, e a equipe acredita que a aplicação da política está funcionando.

Então, Claude Code lança uma nova versão. PermissionRequest começa a disparar junto com PreToolUse. O servidor de hook retorna o mesmo JSON permissionDecision que já estava retornando. Claude Code aceita a resposta, faz o parsing, não encontra nenhum campo que reconheça para aquele evento, e prossegue como se nenhuma decisão tivesse sido dada. A negação é silenciosamente ignorada. O ponto de aplicação da política da equipe agora é um muro com gap nele, e nada nos logs indica isso.

Este é o problema que um PEP governado precisa resolver: o ciclo de vida do hook do Claude Code não é um único evento com um formato de fio único. São aproximadamente 30 eventos, cada um com um esquema de saída diferente que o runtime reconhece, e um erro no esquema é indistinguível do silêncio.

O mecanismo de fio é o contrato de segurança

A razão pela qual um único campo permissionDecision não funciona em todos os lugares é que os eventos de hook do Claude Code evoluíram de forma independente. A forma de saída que controla uma chamada de ferramenta não é a forma que controla o envio de um prompt, que não é a forma que interrompe uma tarefa.

Existem seis mecanismos de fio distintos:

MecanismoForma de saídaEventos
permissionDecisionhookSpecificOutput.permissionDecision (permitir /deny/ask/defer)UsoAvantFerramenta
permissionBehaviorhookSpecificOutput.decision.behavior (permitir /deny)SolicitaçãoDePermissão
topLevelDecisionNível superior decision: "block" + reasonEnvioDePromptDoUsuário, ExpansãoDePromptDoUsuário, PreCompactação, MudançaDeConfiguração, PósLoteDeFerramenta
continueFalsecontinue: false + stopReasonTarefaCriada, TarefaConcluída, ColegaDeEquipeOcioso
postToolUsedecision: "block" (bloqueia processamento adicional; ferramenta já executada)PósUsoDeFerramenta
neutralNenhum bloqueio aplicávelTodos os eventos context/observe, além de eventos invertidos como Stop

Se um PEP retornar permissionDecision: "deny" em um evento PermissionRequest, Claude Code o ignora — esse evento espera decision.behavior, não permissionDecision. O JSON é válido, a resposta HTTP é 200, e a negação não existe. Este não é um caso limite teórico; é o contrato de comunicação documentado, verificado contra code.claude.com/docs/en/hooks.

Classificação em três vias: controle, contexto, observação

Nem todo evento de hook pode controlar uma ação. Tentar negar um SessionEnd ou um Notification é sem sentido — esses eventos são informativos e não carregam controle de decisão. Tentar negar um evento Stop é pior que sem sentido: decision: "block" em Stop mantém o agente funcionando, o que é o oposto de uma parada de segurança.

O PEP classifica cada evento de hook reconhecido em uma das três categorias:

Eventos de Gating carregam uma decisão que pode permitir, negar ou bloquear uma ação. Eles são a superfície de aplicação. Mas nem todos os eventos de gating são igualmente aplicáveis: Stop e SubagentStop são classificados como gating na própria taxonomia de Claude Code, mas sua semântica de bloqueio é invertida (bloquear = continuar executando), então o PEP os trata como neutros — ele nunca emite um bloqueio que manteria um agente vivo contra a intenção do operador. De forma semelhante, Elicitation e ElicitationResult têm uma classificação de gating, mas não possuem um mecanismo de ação conectado na versão atual, então o PEP os define como neutros em vez de fingir que a aplicação existe onde ela não existe.

Contexto eventos permitem que o PEP injete additionalContext ou reescreva a saída, mas não podem realmente bloquear a ação. PostToolUse é a exceção partial — ela pode bloquear o processamento futuro de uma saída sinalizada, mas a ferramenta já foi executada. O resto (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) são de observar-com-contexto: úteis para enriquecer a visão do modelo, não para pará-lo.

Observar eventos (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) não têm controle de decisão algum. O PEP os registra para o caminho de telemetria e o inventário SIEM, responde de forma neutra e segue em frente.

Esta classificação não é consultiva. Ela determina se o decidor da raiz de composição aplica uma regra de política (gating + aplicável), injeta contexto (contexto) ou apenas observa (observar). Errar significa ou aplicação falsa (retornando um negar que é ignorado) ou aplicação perdida (tratando um evento de gating como observar).

O mapa hookSpecs: fonte única da verdade

A classificação, o mecanismo de conexão e a flag de aplicabilidade vivem em um mapa. Este é o código real que o conector usa — tanto o renderizador de resposta HTTP quanto o decidor da raiz de composição leem a partir dele:

type hookSpec struct {
    class       string   // "gating" | "context" | "observe"
    mech        hookMech
    enforceable bool
}

var hookSpecs = map[string]hookSpec{
    // GATING — o retorno de um hook pode aplicar allow/deny/block à ação.
    "PreToolUse":          {"gating", mechPermissionDecision, true},
    "PermissionRequest":   {"gating", mechPermissionBehavior, true},
    "UserPromptSubmit":    {"gating", mechTopLevelDecision, true},
    "UserPromptExpansion": {"gating", mechTopLevelDecision, true},
    "PreCompact":          {"gating", mechTopLevelDecision, true},
    "ConfigChange":        {"gating", mechTopLevelDecision, true},
    "PostToolBatch":       {"gating", mechTopLevelDecision, true},
    "TaskCreated":         {"gating", mechContinueFalse, true},
    "TaskCompleted":       {"gating", mechContinueFalse, true},
    "TeammateIdle":        {"gating", mechContinueFalse, true},
    "Stop":                {"gating", mechNeutral, false}, // invertido
    "SubagentStop":        {"gating", mechNeutral, false}, // invertido
    "Elicitation":         {"gating", mechNeutral, false}, // não ligado na v1
    "ElicitationResult":   {"gating", mechNeutral, false},
    // CONTEXT — additionalContext / reescrita da saída, sem bloqueio real.
    "PostToolUse":        {"context", mechPostToolUse, true},
    "PostToolUseFailure": {"context", mechNeutral, false},
    "PermissionDenied":   {"context", mechNeutral, false},
    "MessageDisplay":     {"context", mechNeutral, false},
    "SessionStart":       {"context", mechNeutral, false},
    "Setup":              {"context", mechNeutral, false},
    "SubagentStart":      {"context", mechNeutral, false},
    "PostCompact":        {"context", mechNeutral, false},
    "InstructionsLoaded": {"context", mechNeutral, false},
    // OBSERVE — sem controlo de decisão.
    "Notification":   {"observe", mechNeutral, false},
    "SessionEnd":     {"observe", mechNeutral, false},
    "StopFailure":    {"observe", mechNeutral, false},
    "CwdChanged":     {"observe", mechNeutral, false},
    "FileChanged":    {"observe", mechNeutral, false},
    "WorktreeCreate": {"observe", mechNeutral, false},
    "WorktreeRemove": {"observe", mechNeutral, false},
}

Trinta eventos, cada um com exatamente uma classificação, um mecanismo de fio e um veredito de aplicabilidade. O renderizador consulta hookMechFor(event) para decidir qual formato JSON emitir. O decisor consulta HookEnforcementFor(event) para decidir se deve aplicar uma regra de política, injetar contexto ou observar. Ambos leem do mesmo mapa, então não podem discordar.

Hook PEP de negação padrão: fluxo de classificação de evento e mecanismo de fio

O padrão de negação

A função mais importante no arquivo tem quatro linhas de comprimento:

func hookSpecFor(event string) hookSpec {
    if s, ok := hookSpecs[event]; ok {
        return s
    }
    return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}

Um evento que não está no mapa — porque Claude Code enviou um novo evento de gancho que o conector ainda não classificou — é tratado como unknown, atribuído ao mecanismo de fio mechPermissionDecision e marcado como aplicável. Este é o padrão de negação-fechada: um evento não reconhecido é um portão de permissão, não uma passagem silenciosa. Se nenhuma regra de política corresponder, aplica-se a postura padrão configurada pelo operador, que em uma implantação governada é negar.

A alternativa — definir como neutro ou observar — significaria que cada novo evento de gancho que Claude Code introduz ficaria sem controle até que alguém o notasse e adicionasse ao mapa. Em um modelo de negação-fechada, o novo evento é governado desde o momento em que ocorre, mesmo que a classificação seja conservadora. Uma negação falsa em um novo evento é visível e corrigível; uma permissão silenciosa é invisível e pode persistir por meses.

A struct HookEnforcement exporta esta classificação para que o decisor possa distinguir três níveis de eventos de gate:

  • Gate clássico (PreToolUse, PermissionRequest, PostToolUse e qualquer evento desconhecido): quando nenhuma regra governada é correspondida, o padrão da política do operador se aplica (negar-fechado).
  • Gate de ciclo de vida (outros eventos de gate aplicáveis, como UserPromptSubmit, TaskCreated): o decisor aplica um padrão seguro por evento — neutro para eventos UX/lifecycle, negar para eventos de mutação de estado.
  • Gate não aplicável (Stop, SubagentStop, Elicitation): o decisor retorna neutro independentemente. Emitir um negar que Claude Code interpreta como “continuar executando” seria o oposto de seguro.

Distribuição: da classificação para a frota

Classificar eventos corretamente é uma metade. Colocar o PEP em cada instância Claude Code é a outra metade. O conector de configurações gerenciadas transforma a configuração do hook na forma que Claude Code espera e a distribui como um arquivo de configurações gerenciadas pelo servidor — uma configuração não substituível que o plano de controle envia para cada host gerenciado.

O hook PEP é distribuído com um matcher vazio (corresponde a todas as ferramentas — nenhuma ferramenta escapa do ponto de aplicação) e o associa com allowManagedHooksOnly para evitar que hooks locais de um desenvolvedor neutralizem o PEP gerenciado. Sem esse flag, um hook de nível de usuário poderia sobrepor o gerenciado, e a aplicação pareceria presente enquanto poderia ser contornada. A validação no momento da autoria detecta isso: se uma política envia um hook PEP PreToolUse sem allowManagedHooksOnly, o console emite um aviso de anti-manipulação.

O caminho de telemetria é separado e não bloqueado por plano: a exportação OpenTelemetry de Claude Code é habilitada por meio de variáveis de ambiente gerenciadas (CLAUDE_CODE_ENABLE_TELEMETRY, as chaves de exportador OTEL_*) para que o plano de controle possa observar o uso da assinatura sem fazer proxy de inferência ou acessar a credencial da assinatura. A captura de conteúdo (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) é desativada por padrão; ativá-la é uma escolha deliberada e sinalizada porque envia o conteúdo de prompt e ferramenta para fora da máquina do desenvolvedor, criando uma obrigação de residência e redação que o plano de controle deve assumir.

O que isso significa para uma implantação regulamentada

Uma equipe executando Claude Code com um PEP regulamentado obtém algumas propriedades que importam:

  1. Sem permissão silenciosa. Todo evento de hook é classificado, e qualquer evento não reconhecido tem como padrão a negação. Um novo lançamento Claude Code não pode introduzir um evento de ciclo de vida sem governança.
  2. Formato de wire correto por evento. O PEP não retorna um objeto JSON genérico esperando que o Claude Code o aceite. Ele retorna o esquema de saída exato que o evento específico espera, porque uma negação no esquema errado não é uma negação.
  3. Não aplicação honesta. Eventos que não podem ser aplicados (eventos de observação, eventos de bloqueio invertido) não são aplicados de forma fictícia. O PEP os registra, retorna neutro e não dá ao operador uma falsa sensação de controle.
  4. Anti-fraude na distribuição. A camada de configurações gerenciadas garante que o hook do PEP não possa ser sobrescrito e sinaliza configurações onde ele poderia ser comprometido.

O PEP é a metade de aplicação de um modelo operacional governado maior. O mapa de acesso, o registro de auditoria e a camada de política-como-código ao redor dele são abordados na visão geral do produto e na documentação de hooks. Se você quiser ver como o caminho de telemetria e o portão de permissões se compõem, a visão geral da arquitetura percorre ambos.

Artigos relacionados

Perguntas frequentes

Por que o PEP define por padrão eventos de hook desconhecidos como deny em vez de allow?

Uma permissão silenciosa em um evento não reconhecido significa que qualquer novo hook Claude Code incluído em uma versão futura passaria sem restrições até que alguém o adicionasse manualmente ao mapa de classificação. Isso é o oposto da postura de segurança que uma implantação governada necessita. O padrão deny-closed trata um evento desconhecido como um portão de permissão: ele é observado, nunca descartado, e se nenhuma regra de política corresponder, ele é negado. Isso garante que novos eventos do ciclo de vida sejam governados desde o momento em que aparecem, e não a partir do momento em que alguém percebe que eles não foram.

O que acontece se o PEP emitir o formato de dados incorreto para a resposta de um hook?

Claude Code ignora silenciosamente. Cada evento de hook respeita um esquema de saída específico: PreToolUse espera hookSpecificOutput.permissionDecision, PermissionRequest espera hookSpecificOutput.decision.behavior, e outros eventos de bloqueio esperam um campo de decisão de nível superior ou continue. Se o PEP retornar um objeto JSON válido, mas no esquema errado para aquele evento, Claude Code o trata como nenhuma decisão e prossegue. É por isso que o mecanismo de fio faz parte do contrato de segurança, não apenas como um detalhe estético: uma negação que Claude Code ignora não é uma negação.

Veja a que os seus agentes conseguem aceder

A Olivares AI é a plataforma aberta e self-hosted para o seu ecossistema de IA. Implemente-a na sua própria infraestrutura e obtenha o mapa de acessos que as suas equipas de segurança e plataforma têm vindo a pedir.