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.
Resumo
O hook
enforce-git-push-authority.cjs, que implementa o Artigo II da Constitution (git pushexclusivo do@devops), pode ser contornado de duas formas independentes. Ambas foram reproduzidas localmente alimentando o hook via stdin, sem executar nenhumgit.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.0Ambiente: Linux x64 (WSL2), Node 18+, Claude Code
Arquivo:
.claude/hooks/enforce-git-push-authority.cjsProblema 1 — a identidade do agente é lida do texto do comando
getActiveAgent(command)incluigetCommandScopedAgent(command)entre os candidatos:getCommandScopedAgentextraiAIOX_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
O padrão exige
gitimediatamente seguido depush. 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: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 reservagit push,gh pr createegh pr mergeao@devops. Com esses dois furos, a regra não é aplicada na prática — qualquer agente publica sem passar pelos quality gates que o@devopsexecuta antes do push.Vale notar também que
gh release create, listado como exclusivo nogit_authoritydo agente devops, não está emREMOTE_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:
git commit -m "wip && git push depois"é quebrado em dois e o commit legítimo acaba bloqueado.nice -n 10 git push,timeout 30 git pushesudo -u alguem git pushescapam se o parser pular a flag mas parar no valor dela. Em vez de enumerar as flags de cada wrapper, é mais robusto seguir procurandogit/ghao longo do segmento depois de ver um wrapper — mantendo a busca limitada a esse caso, paraecho git pushnão virar falso positivo.ghconsomem valor. Avançar dois tokens por palpite fazgh --verbose pr createescapar, porque opré 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,commitcom 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.