Diagrama de arquitetura — plano cliente de hooks git consultivos × plano servidor com ruleset do GitHub; o caminho --no-verify pula os hooks mas esbarra na mesma parede do servidor
pedro.balbinoblog.home301server.com.br · FLEET ENGINEERING · git governanceSERVER RULESETS + CLIENT HOOKSdefense-in-depth for one solo dev · N coding agents · hooks are advice, rulesets are lawfig. 1 — Container view · git governancerev. 2026-07-11CLIENT PLANE · local, fast, bypassableN agent worktrees · lefthook pre-commit · git pre-push · advisory, skippable with --no-verifyN agent worktreesone worktree per PR · isolated branchcommit stream →lefthook · pre-commitconventional-commit · stealth · lint · frontmatter · diagram-qcADVISORY — LEFTHOOK_EXCLUDE / --no-verify skips itsigned commitSSH-signed · Conventional Commits · ~0.19s sweepgit · pre-push hookcross-host eval · no-push-to-main · force-push descendant guardADVISORY — snapshots dropped SHAs → refs/backupruns · warns · non-bindingrefs/backup + reflogpost-commit mirror · per-host namespace · 180d / 90d unreachable--no-verify (skips hooks, NOT the server)git pushSERVER PLANE · remote, enforced, bindingGitHub ruleset · required checks · linear history · signed commits · no force-push · bypass_actors: noneGitHub RULESETenforcement: active · bypass_actors: none (admins included)required status checks · strict up-to-daterequired linear historyrequired signatures · SSHno force-push · no deletionreview-thread resolution0 required approvals · no self-approve--no-verify arrives here like every push · the server does not care whether your hooks ranmainprotected · merge iff every rule passespassMEASURED · this repo's own gates~0.19spre-commit sweep258-file tree · sub-50ms staged5–8spre-push cross-host evalwarm · 30–60s cold3required status checksthe only gates that block merge0required approvalssolo dev can't self-approve180dreflog recovery windowdropped-commit safety netANTI-PATTERN · --no-verify culture — treating advisory hooks as the only gatestack: GitHub rulesets · lefthook · git hooks · Conventional Commits · SSH-signed commits · CI required checksfig. 1 — Container view · git governance
Diagrama de arquitetura — plano cliente de hooks git consultivos × plano servidor com ruleset do GitHub; o caminho --no-verify pula os hooks mas esbarra na mesma parede do servidor

Um repositório. Vários agentes de código. Uma branch que precisa estar sempre pronta pra deploy.

O setup inteiro é esse, e ele é mais estranho do que parece escrito. Os agentes rodam em worktrees paralelas — uma por pull request — e cada um consegue commitar, assinar, dar push, fazer rebase e, se decidir que os checks locais estão no caminho, pular todos. Eles são rápidos. Acertam quase sempre. E o “quase” é o problema todo: a falha pra qual você projeta não é o agente que trava, é o que está errado com confiança às 3 da manhã, sem ninguém olhando.

Aí você recorre aos git hooks . Pre-commit linta. Pre-push roda o build. Commit-msg cobra o formato. Por um tempo parece governança. Não é. É conselho — e qualquer coisa que rode git pode recusar conselho.

O hook que dá pra pular não é gate

A descrição honesta de um git hook: um script que o cliente roda em seu nome, quando está a fim. --no-verify pula pre-commit e commit-msg num flag só. LEFTHOOK_EXCLUDE=lint derruba um comando específico. Apague .git/hooks/ e todo hook some; o repositório nem fica sabendo. Um agente que ouviu uma única vez que “o eval do pre-push é lento” acha o --no-verify sozinho. Gate que o ator consegue dispensar com um aceno não é gate — é sugestão bem-intencionada.

As restrições de dev solo afiam o corte. Não existe segundo humano pra revisar: você não pode aprovar o próprio pull request, e não tem mais ninguém. O loop local tem orçamento de latência — se o sweep de pre-commit custa dez segundos, os agentes desviam dele, e check que ninguém roda não protege nada. E o que está sob guarda é uma branch que deploya: um push ruim é evento de produção, não orgulho ferido. Eu já vi a falha exata: um fix estrutural encalhado numa troca de branch, descartado em silêncio, o build seguinte quietamente sem ele. Sem erro. Só sumiu.

O Y-statement que assinei:

No contexto de um dev solo rodando vários agentes de código contra uma única branch que deploya, enfrentando atores capazes de burlar qualquer check local e sem um segundo humano pra pegar o erro, decidimos dividir a imposição entre hooks client-side consultivos pela latência e um ruleset server-side vinculante pela integridade, para obter uma branch sempre pronta pra release sem frear o inner loop, aceitando que os hooks são teatro a menos que o servidor imponha os mesmos invariantes por baixo.

Duas camadas, dois trabalhos

A divisão não é redundância. É divisão de trabalho: os hooks compram velocidade, o ruleset compra a garantia. Nenhum faz o trabalho do outro, e é confundindo os dois que repositório acaba “protegido” por um script que todo contribuidor já aprendeu a pular.

No lado do cliente, os hooks são conselho que você realmente quer seguir. O lefthook espalha o sweep de pre-commit em paralelo — formato de conventional commit, varredura de segredo, formatador, o lint do próprio projeto — e limpa uma árvore de 258 arquivos em 0.19s, um commit staged de um a três arquivos em sub-50ms. Rápido o bastante pra ninguém se ressentir, que é a única razão de sobreviver. Um hook de pre-push roda o check caro — um eval cross-host, 5-8s quente, 30-60s depois de um bump de dependência — e recusa os dois movimentos que mais custam pra desfazer: push direto na main e force-push que não é descendente fast-forward. Quando a história é reescrita de verdade, um hook de post-commit já espelhou o tip antigo pra refs/backup/<host>/<branch>, e o reflog segura commit descartado por 180d. Conselho, com rede embaixo.

No lado do servidor, o ruleset é a lei. Não “deveria”, não “por favor” — o servidor recusa o push. Um ruleset de repositório do GitHub codifica os invariantes que não dobram: required_status_checks com strict up-to-date, required_linear_history, required_signatures , non_fast_forward e proibição de deleção, resolução de thread de review e — a linha que se lê como sentença — bypass_actors: []. Ninguém passa por cima. Admin incluído.

A fitness function não é o ruleset. É a checagem de que o ruleset continua em vigor:

bash
# fitness gate — the law must still be enforced, not merely written down.
# Run it in CI: a ruleset that quietly flips to disabled is a hook nobody runs,
# except this failure mode you can actually detect.
gh api "repos/$REPO/rulesets/$RULESET_ID" --jq '
  (.enforcement == "active")
  and (.bypass_actors | length == 0)          # nobody bypasses, admins included
  and (.rules | map(.type) | contains([
        "required_signatures",
        "non_fast_forward",
        "required_linear_history",
        "pull_request",
        "required_status_checks"
      ]))
' | grep -qx true || { echo "ruleset drifted — main is unguarded"; exit 1; }

Um detalhe que a doc enterra: o required_status_checks do ruleset só pode nomear jobs que o seu CI realmente produz. Liste um check que nunca roda e o ruleset tranca a main atrás de um status eternamente pendente — você impôs um deadlock, não uma política.

As decisões, com preço

Hooks consultivos, nunca mandatórios. Custo: dá pra pular, por construção. Benefício: nunca te trancam fora do próprio repositório no meio de um incidente, e continuam rápidos o bastante pra manter. Reconsiderar: nunca — enquanto o servidor impuser a mesma regra. No instante em que um hook é a única coisa entre um agente e a main, ele já falhou; a conta só não chegou ainda.

Ruleset, não as branch-protection rules legadas. Rulesets empilham, são reutilizáveis na org inteira e deixam a lista de bypass explícita. bypass_actors: [] é prosa auditável. A UI antiga de branch protection escondia o “admins podem passar” num checkbox que ninguém relê — exatamente onde mora o próximo incidente.

Zero aprovações obrigatórias. Parece errado até você dizer em voz alta: dev solo não pode aprovar o próprio pull request, então regra exigindo uma aprovação humana é regra exigindo que você nunca faça merge. O revisor vira o required status check, não uma pessoa — 0 aprovações em vez de 1. Custo: sem carimbo humano. Benefício: o merge de fato acontece, guardado por um check que não dorme, não tira férias e não carimba no automático às 3 da manhã.

O anti-padrão atravessa as três decisões, e tem nome: cultura de --no-verify — tratar o hook local como o gate e depois se surpreender quando algo passa por fora. O primo dele é o bypass de admin, aquela entrada em bypass_actors que você adiciona “só dessa vez” durante um incidente e nunca remove. Os dois movem a fronteira real do servidor pra um hábito, em silêncio. Hábito não sobrevive a uma noite cansada; recusa do servidor sobrevive.

O que o hook não consegue prometer

Hooks mentem, estruturalmente — não por malícia, mas porque rodam no cliente, e o cliente é justamente o que você está tentando conter. Um commit assinado prova quem foi o autor. Não prova que o pre-commit rodou. Só o servidor, recusando o push, prova algo que o autor não poderia forjar — e essa é a razão inteira de o servidor segurar os invariantes que importam.

É também por isso que imposição que briga com o operário acaba deletada. Eu já rodei um guard de pre-tool que bloqueava agente de trocar de branch dentro de worktree; ele pegava erro de verdade e também bloqueava trabalho legítimo, então caiu, e a disciplina que ele impunha virou regra escrita. Essa é a lição de verdade, e ela corta contra o reflexo de adicionar mais hooks: guard local hostil o bastante pra valer a pena burlar vai ser burlado — ou removido pela pessoa que ele atrasa, que é você. Ponha no servidor os invariantes que não podem dobrar, onde dobrar não está no cardápio, e mantenha a camada local útil o bastante pra ninguém querer desligar.

Quando isso é exagero? Repositório com um humano cuidadoso e zero agente não precisa da cerimônia — um hook de pre-commit e uma branch protection simples bastam. Defense-in-depth se paga exatamente quando o ator de quem você se guarda é rápido, numeroso e de vez em quando erra num horário em que nenhum revisor está acordado pra pegar. É nessa sala que uma frota de agentes te coloca.

Os hooks deixam o caminho bom rápido. O ruleset torna o caminho ruim impossível. Só um dos dois é negociável — e não é o que mora no servidor.

Stack

  • Hooks locais: lefthook com fan-out paralelo do pre-commit + um punhado de git hooks declarativos (guard de pre-push, espelho de backup no post-commit, prepare-commit-msg)
  • Formato de commit: Conventional Commits — cobrado pelo commit-msg local e re-checado como required status check no CI, pro passe local nunca segurar nada sozinho
  • Assinatura: commits assinados por SSH, impostos no servidor via required_signatures
  • Lei do servidor: ruleset de repositório do GitHub — enforcement: active, bypass_actors: [], required status checks (strict up-to-date), histórico linear obrigatório, sem force-push, sem deleção, resolução de thread de review, 0 aprovações obrigatórias
  • Recuperação: espelho refs/backup/<host>/<branch> no post-commit + retenção de reflog (180d / 90d pra objetos inalcançáveis)
  • CI: os required checks que são o gate de merge de verdade — lint e formatação, saúde de dependências, eval por host