Skip to content

enforce-git-push-authority: Artigo II contornável por prefixo no comando e por opções globais do git #817

Description

@simplycontabilidade

Resumo

O hook enforce-git-push-authority.cjs, que implementa o Artigo II da Constitution (git push exclusivo do @devops), pode ser contornado de duas formas independentes. Ambas foram reproduzidas localmente alimentando o hook via stdin, sem executar nenhum git.

Na prática o controle não impede nenhum agente de publicar: basta escrever o prefixo certo ou usar uma opção global do git.

Versão: @aiox-squads/core-internal@5.3.0
Ambiente: Linux x64 (WSL2), Node 18+, Claude Code
Arquivo: .claude/hooks/enforce-git-push-authority.cjs


Problema 1 — a identidade do agente é lida do texto do comando

getActiveAgent(command) inclui getCommandScopedAgent(command) entre os candidatos:

function getActiveAgent(command) {
  const candidates = [
    process.env.AIOX_ACTIVE_AGENT,
    // ... demais env vars
    getCommandScopedAgent(command),   // <-- lê do próprio comando
  ];
  return String(candidates.find(Boolean) || '').toLowerCase();
}

getCommandScopedAgent extrai AIOX_ACTIVE_AGENT=... por regex sobre a string do comando. O resultado é que o comando autoriza a si mesmo: qualquer agente que escreva o prefixo passa pelo hook.

Isso importa porque, num projeto recém-instalado, nenhuma das variáveis de ambiente está definida na sessão. O texto do comando acaba sendo a única via pela qual o hook consegue identificar alguém como @devops — ou seja, o caminho vulnerável é o caminho normal.

Problema 2 — a detecção ignora as opções globais do git

{ pattern: /\bgit\s+push\b/i, operation: 'git push' }

O padrão exige git imediatamente seguido de push. Qualquer opção global entre os dois quebra o match, e o hook simplesmente não vê a operação.


Reprodução

Alimentando o hook diretamente, sem executar git:

testa() {
  printf '{"tool_name":"Bash","tool_input":{"command":"%s"}}' "$1" \
    | node .claude/hooks/enforce-git-push-authority.cjs
}

testa 'git push origin master'                        # -> deny (esperado)
testa 'AIOX_AGENT=devops git push origin master'      # -> passa  (problema 1)
testa 'git -C /tmp/x push origin master'              # -> passa  (problema 2)
testa 'git -c protocol.version=2 push origin master'  # -> passa  (problema 2)
testa 'git --git-dir=/tmp/x/.git push origin master'  # -> passa  (problema 2)

Só o primeiro caso é bloqueado. Os quatro seguintes publicam normalmente.


Impacto

O Artigo II é declarado NON-NEGOTIABLE e é a base da matriz de autoridade (agent-authority.md), que reserva git push, gh pr create e gh pr merge ao @devops. Com esses dois furos, a regra não é aplicada na prática — qualquer agente publica sem passar pelos quality gates que o @devops executa antes do push.

Vale notar também que gh release create, listado como exclusivo no git_authority do agente devops, não está em REMOTE_OPERATION_PATTERNS.


Sugestão de correção

Apliquei localmente o seguinte, que resolve os dois casos:

1. A identidade nunca vem do comando. getActiveAgent() deixa de receber o comando e passa a considerar apenas variáveis de ambiente reais do processo, mais um arquivo de sessão (.aiox/active-agent, gitignored) escrito como ato deliberado ao ativar o @devops.

2. Detecção por tokenização em vez de regex. Um tokenizador ciente de aspas (simples, duplas e escape) quebra o comando em segmentos, descarta atribuições VAR=valor, pula as opções globais do git (-C, -c, --git-dir, --work-tree, --namespace, --exec-path, --config-env, --super-prefix, na forma com e sem =) e só então lê o subcomando. Cobre caminho absoluto (/usr/bin/git push) e encadeamento (npm test && git push).

Três detalhes que custaram uma segunda rodada aqui, e que valem para quem for implementar:

  • Aspas não podem ser ignoradas. Sem elas, git commit -m "wip && git push depois" é quebrado em dois e o commit legítimo acaba bloqueado.
  • Wrappers consomem argumentos. nice -n 10 git push, timeout 30 git push e sudo -u alguem git push escapam se o parser pular a flag mas parar no valor dela. Em vez de enumerar as flags de cada wrapper, é mais robusto seguir procurando git/gh ao longo do segmento depois de ver um wrapper — mantendo a busca limitada a esse caso, para echo git push não virar falso positivo.
  • Não adivinhe quais flags do gh consomem valor. Avançar dois tokens por palpite faz gh --verbose pr create escapar, porque o pr é engolido como se fosse valor da flag. Testar por presença das palavras (pr + create) resolve sem heurística.

Validado com 39 casos: tentativas de bypass bloqueadas e comandos legítimos preservados (git status, log, commit com mensagem contendo "git push", fetch, gh pr list, echo "git push" > notas.txt), mais a sessão declarada liberando e negando conforme o agente.

Posso enviar como PR se for útil.


Uma observação sobre o alcance da proteção

Mesmo corrigido, isto continua sendo um guardrail de processo, não um controle de segurança: um agente com acesso a Bash sempre poderá escrever o arquivo de sessão e se declarar @devops. Nenhum mecanismo executado pelo próprio agente consegue impedir o agente.

O ganho real da correção é outro — a declaração deixa de ser um prefixo invisível na mesma linha do comando que se quer autorizar, e passa a ser um ato separado, explícito e auditável. Talvez valha deixar isso explícito na documentação do Artigo II, para que a garantia oferecida não seja superestimada por quem depende dela.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: cliCLI tools (bin/, packages/aios-pro-cli/)status: needs-triageAwaiting initial triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions