Summary
Em repositórios que usam Husky v9 (core.hooksPath=.husky/_), o Iris instala os hooks dentro do stub interno do Husky — um arquivo que sempre encerra antes de alcançar o código injetado. O iris hook install reporta sucesso, o iris hook status diz installed: true, mas nenhum trailer de atribuição é escrito e nenhum push automático acontece. O resultado é que commits assistidos por IA entram na base como HUMAN, de forma permanente e não recuperável.
Origem: investigação do @alexbuenocb compartilhada em #eng-ai-produtividade (documento completo).
Três defeitos independentes compõem a falha:
Bug A — código do hook nunca executa (crítico).
O Husky v9 usa delegação em duas camadas. core.hooksPath aponta para .husky/_, cujos arquivos são stubs de 39 bytes:
#!/usr/bin/env sh
. "$(dirname "$0")/h"
O h é sourced (.), roda sh -e .husky/<hook> e termina em exit $c. Como exit encerra o shell que fez o source, tudo que o Iris anexa depois dessa linha é código morto. get_hooks_dir() (iris/hooks/manager.py:46-62) devolve core.hooksPath sem detectar essa indireção.
Agravante: .husky/_/.gitignore contém * — o diretório é regenerado a cada npm install, então mesmo o código morto é apagado periodicamente. É o que explica design-system-react estar hoje sem hook nenhum, apesar de já ter sido instalado.
Bug B — flag inexistente no hook de push.
iris/hooks/post_commit_push.sh:49 chama "$IRIS_BIN" "$REPO_ROOT" --push --quiet. O flag --quiet não existe no argparse do CLI (v1.5.0 aceita --verbose, não --quiet). O argparse rejeita o comando inteiro com exit 2, e o 2>/dev/null engole o usage:, então a falha é totalmente silenciosa e o stamp file nunca é escrito — o hook tenta de novo a cada commit, sempre falhando.
Bug C — stdout não é redirecionado.
Ainda na linha 49, só o stderr vai para /dev/null. O bloco roda em background (&) e leva ~10-15s; quando o descritor de stdout do commit fecha antes disso, o processo iris recebe erro de pipe e morre antes de gravar o stamp.
Bug D — chaves de trailer: já resolvido, não refazer.
O documento original pedia expandir a regex para aceitar Assisted-by:. Isso já está em produção desde que _ATTRIBUTION_TRAILER_RE (iris/ingestion/git_reader.py:40-43) passou a ler Co-authored-by|Assisted-by|Made-with. Verificado nos repos afetados: 100% dos trailers reais são capturados, incluindo variações de modelo e de caixa. Evidência na seção de logs.
Reproduction steps
- Criar um repo com layout Husky v9:
git init repro && cd repro && mkdir -p .husky/_
cp <qualquer-repo-husky9>/.husky/_/h .husky/_/h
printf '#!/usr/bin/env sh\n. "$(dirname "$0")/h"' > .husky/_/prepare-commit-msg
chmod +x .husky/_/h .husky/_/prepare-commit-msg
git config core.hooksPath .husky/_
iris hook install e iris hook status.
- Commitar com o agente sinalizado:
echo hi > a.txt && git add a.txt && AI_AGENT="Claude Code" git commit -m "test: repro".
- Inspecionar o corpo do commit:
git log -1 --format='%B'.
Expected behaviour
O hook deveria ser instalado onde o Husky realmente executa código (.husky/<hook>, o hook público), o commit deveria receber Co-Authored-By: Claude Code <noreply@anthropic.com>, e o status deveria refletir alcançabilidade real de execução — não apenas presença do marcador.
Quando a instalação não for alcançável, status deve reportar não instalado (ou degradado), nunca installed: true.
Actual behaviour
Repro executado localmente contra o h real do design-system-react:
get_hooks_dir -> <repo>/.husky/_
installed at -> <repo>/.husky/_/prepare-commit-msg
status -> {'installed': True, 'hook_type': 'prepare-commit-msg', ...}
--- corpo do commit resultante ---
test: repro$
$
Nenhum trailer. O commit é ingerido como HUMAN e não há como reatribuí-lo depois sem reescrever histórico.
Estado atual dos repos locais (core.hooksPath × onde o Iris gravou):
| Repo |
core.hooksPath |
Hook do Iris está em |
Executa? |
% AI (200d) |
clickbus-platform-bff |
.husky/_ |
.husky/_/ |
❌ código morto |
15% |
design-system-react |
.husky/_ |
(nenhum — apagado) |
❌ ausente |
52% |
clickbus-frontend-responsive |
(vazio) |
.git/hooks/ |
✅ |
11% |
voucher, pricing |
(vazio) |
.git/hooks/ |
✅ |
60% / 88% |
Os 52% do design-system-react vêm de trailers escritos manualmente pela instrução global do agente, não pelo hook — ou seja, a cobertura real do hook nesses repos é zero.
Surface
Hooks (prepare-commit-msg)
Versão do CLI
v1.5.0
Repo analisado
RocketBus/design-system-react, RocketBus/clickbus-platform-bff, RocketBus/clickbus-web-kit
Logs / screenshots / trace-id
Bug B, reproduzido:
$ iris . --push --quiet
usage: iris [-h] [--org ORG] [--repos REPOS] [--days DAYS] [--windows WINDOWS]
[--churn-days CHURN_DAYS] [--out OUT] [--lang {en,pt-br}]
[--trend] [--recent-days RECENT_DAYS] [--push] [--no-push]
[--verbose]
[repo_path]
Bug D — evidência de que as chaves de trailer já funcionam. Varredura com _ATTRIBUTION_TRAILER_RE + classify_origin sobre o histórico real (200 dias, 7 repos): nenhum trailer de atribuição escapou. As formas abaixo, todas presentes no histórico, classificam como AI_ASSISTED / tool Claude:
Assisted-by: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: claude-code_2-1-183_agent <claude-code_2-1-183_agent@iris.invalid>
A coluna "mention-no-trailer" da varredura (commits cujo corpo cita uma ferramenta de IA mas não tem trailer) só retornou prosa — docs: add CLAUDE.md for Claude Code e similares — corretamente não atribuídos.
Impacto nos dados. Commits com trailer já contam corretamente hoje; nada a recuperar ali. Commits sem trailer nenhum (onde o hook morto deveria ter escrito) são permanentemente não atribuíveis — a correção só vale dali para frente. Quanto mais tempo o bug ficar aberto, maior o buraco no histórico dos repos com Husky.
Notas para a correção
get_hooks_dir() precisa detectar a indireção do Husky (presença de _/h ou _/husky.sh dentro de core.hooksPath) e instalar no diretório pai.
- O
h do Husky roda o hook público com sh -e — qualquer comando de topo que devolva não-zero aborta o commit. O prepare_commit_msg.sh usa grep -q no guard de já-atribuído; sob -e isso derruba o commit quando não há match. Os trechos injetados precisam de guarda (|| true) antes de irem para .husky/.
.husky/<hook> é versionado no git, diferente de .git/hooks/. Instalar ali deixa a árvore suja / exige commit do hook. Decidir entre (a) instalar e documentar que o hook é versionado, ou (b) desfazer o core.hooksPath — precisa de decisão antes do PR.
- Migração:
install deve limpar marcadores mortos deixados em .husky/_/.
Summary
Em repositórios que usam Husky v9 (
core.hooksPath=.husky/_), o Iris instala os hooks dentro do stub interno do Husky — um arquivo que sempre encerra antes de alcançar o código injetado. Oiris hook installreporta sucesso, oiris hook statusdizinstalled: true, mas nenhum trailer de atribuição é escrito e nenhum push automático acontece. O resultado é que commits assistidos por IA entram na base comoHUMAN, de forma permanente e não recuperável.Origem: investigação do @alexbuenocb compartilhada em #eng-ai-produtividade (documento completo).
Três defeitos independentes compõem a falha:
Bug A — código do hook nunca executa (crítico).
O Husky v9 usa delegação em duas camadas.
core.hooksPathaponta para.husky/_, cujos arquivos são stubs de 39 bytes:O
hé sourced (.), rodash -e .husky/<hook>e termina emexit $c. Comoexitencerra o shell que fez o source, tudo que o Iris anexa depois dessa linha é código morto.get_hooks_dir()(iris/hooks/manager.py:46-62) devolvecore.hooksPathsem detectar essa indireção.Agravante:
.husky/_/.gitignorecontém*— o diretório é regenerado a cadanpm install, então mesmo o código morto é apagado periodicamente. É o que explicadesign-system-reactestar hoje sem hook nenhum, apesar de já ter sido instalado.Bug B — flag inexistente no hook de push.
iris/hooks/post_commit_push.sh:49chama"$IRIS_BIN" "$REPO_ROOT" --push --quiet. O flag--quietnão existe no argparse do CLI (v1.5.0 aceita--verbose, não--quiet). O argparse rejeita o comando inteiro com exit 2, e o2>/dev/nullengole ousage:, então a falha é totalmente silenciosa e o stamp file nunca é escrito — o hook tenta de novo a cada commit, sempre falhando.Bug C — stdout não é redirecionado.
Ainda na linha 49, só o stderr vai para
/dev/null. O bloco roda em background (&) e leva ~10-15s; quando o descritor de stdout do commit fecha antes disso, o processoirisrecebe erro de pipe e morre antes de gravar o stamp.Bug D — chaves de trailer: já resolvido, não refazer.
O documento original pedia expandir a regex para aceitar
Assisted-by:. Isso já está em produção desde que_ATTRIBUTION_TRAILER_RE(iris/ingestion/git_reader.py:40-43) passou a lerCo-authored-by|Assisted-by|Made-with. Verificado nos repos afetados: 100% dos trailers reais são capturados, incluindo variações de modelo e de caixa. Evidência na seção de logs.Reproduction steps
iris hook installeiris hook status.echo hi > a.txt && git add a.txt && AI_AGENT="Claude Code" git commit -m "test: repro".git log -1 --format='%B'.Expected behaviour
O hook deveria ser instalado onde o Husky realmente executa código (
.husky/<hook>, o hook público), o commit deveria receberCo-Authored-By: Claude Code <noreply@anthropic.com>, e ostatusdeveria refletir alcançabilidade real de execução — não apenas presença do marcador.Quando a instalação não for alcançável,
statusdeve reportar não instalado (ou degradado), nuncainstalled: true.Actual behaviour
Repro executado localmente contra o
hreal dodesign-system-react:Nenhum trailer. O commit é ingerido como
HUMANe não há como reatribuí-lo depois sem reescrever histórico.Estado atual dos repos locais (
core.hooksPath× onde o Iris gravou):core.hooksPathclickbus-platform-bff.husky/_.husky/_/design-system-react.husky/_clickbus-frontend-responsive.git/hooks/voucher,pricing.git/hooks/Os 52% do
design-system-reactvêm de trailers escritos manualmente pela instrução global do agente, não pelo hook — ou seja, a cobertura real do hook nesses repos é zero.Surface
Hooks (prepare-commit-msg)
Versão do CLI
v1.5.0
Repo analisado
RocketBus/design-system-react, RocketBus/clickbus-platform-bff, RocketBus/clickbus-web-kit
Logs / screenshots / trace-id
Bug B, reproduzido:
Bug D — evidência de que as chaves de trailer já funcionam. Varredura com
_ATTRIBUTION_TRAILER_RE+classify_originsobre o histórico real (200 dias, 7 repos): nenhum trailer de atribuição escapou. As formas abaixo, todas presentes no histórico, classificam comoAI_ASSISTED/ toolClaude:A coluna "mention-no-trailer" da varredura (commits cujo corpo cita uma ferramenta de IA mas não tem trailer) só retornou prosa —
docs: add CLAUDE.md for Claude Codee similares — corretamente não atribuídos.Impacto nos dados. Commits com trailer já contam corretamente hoje; nada a recuperar ali. Commits sem trailer nenhum (onde o hook morto deveria ter escrito) são permanentemente não atribuíveis — a correção só vale dali para frente. Quanto mais tempo o bug ficar aberto, maior o buraco no histórico dos repos com Husky.
Notas para a correção
get_hooks_dir()precisa detectar a indireção do Husky (presença de_/hou_/husky.shdentro decore.hooksPath) e instalar no diretório pai.hdo Husky roda o hook público comsh -e— qualquer comando de topo que devolva não-zero aborta o commit. Oprepare_commit_msg.shusagrep -qno guard de já-atribuído; sob-eisso derruba o commit quando não há match. Os trechos injetados precisam de guarda (|| true) antes de irem para.husky/..husky/<hook>é versionado no git, diferente de.git/hooks/. Instalar ali deixa a árvore suja / exige commit do hook. Decidir entre (a) instalar e documentar que o hook é versionado, ou (b) desfazer ocore.hooksPath— precisa de decisão antes do PR.installdeve limpar marcadores mortos deixados em.husky/_/.