20 out 2026 · ia, git, seguranca, ci · 6 min
Como montar uma base segura para um projeto escrito por IA
O esqueleto de um projeto entregue por IA tem 523 linhas, e só 132 delas são código de verdade. As outras 391 são a rede que impede que o resultado entre no ar sem passar por teste e sem a revisão de outra pessoa. Foi a desproporção que impediu um token de entrar no histórico do git.
O ponto do texto não é que “IA precisa de mais cuidado”. Todo código precisa de mais cuidado. O ponto é que um agente escreve rápido demais para que a revisão humana no fim baste: o código chega pronto, passa no olho, e o detalhe que mata está no commit que ninguém leu. A saída é montar a trava antes, não depois.
O que a trava precisa
O AGENTS.md é lido pelo agente no começo de cada tarefa. A página do AGENTS.md explica que ele guarda o que o README não cobre: como rodar os testes, o estilo do código, o que fazer quando uma regra e um pedido no chat se contradizem.
O gancho de commit barra credencial antes do commit existir. O git chama o gancho pre-commit só se core.hooksPath apontar para a pasta onde ele está, e só se o arquivo for executável. Sem as duas coisas, o commit passa em silêncio. O manual dos ganchos descreve o pre-commit como o gancho que roda antes do commit e pode recusá-lo.
A CI roda o portão e exige revisão. O nome do job é o que a proteção de branch aponta: renomear o job sem renomear na configuração deixa a regra apontando para um status check que não existe. A documentação de branch protegida lista “require status checks” e “require pull request reviews” entre as opções, e avisa que admins com bypass passam por cima.
A trava de merge exige que a revisão seja de outra pessoa: um status check verde não diz quem olhou.
A varredura de credencial
A peça que mais assusta é a mais simples: 14 expressões regulares, uma por formato de credencial, e um grep por arquivo.
chave-privada:-----BEGIN [A-Z ]*PRIVATE KEY-----
aws-access-key:AKIA[0-9A-Z]{16}
github-token:gh[pousr]_[A-Za-z0-9]{36}
openai-key:sk-[A-Za-z0-9]{20,}
slack-webhook:hooks\.slack\.com/services/[A-Za-z0-9/]+
O AKIA seguido de 16 caracteres é o formato de chave de acesso da AWS. O ghp_ seguido de 36 caracteres é o token do GitHub.
A documentação de secret scanning do GitHub descreve o mesmo mecanismo: um conjunto de padrões que o GitHub revisa no histórico do repositório, em todas as branches. Em repo público a varredura roda de graça; em repo privado, ela exige o Secret Protection do GitHub Enterprise Cloud.
O gitleaks faz a mesma coisa, com uma lista de regras maior. Ele é open source e roda em qualquer máquina: o secret scanning nativo do GitHub vem com a plataforma, o gitleaks vem com você e roda no seu CI sem depender do plano.
O artefato
Salve como base-segura.sh e rode com o nome do projeto. O script cria a árvore, escreve cada arquivo, instala o gancho, roda o portão e prova que a trava funciona: cria um arquivo de mentira com uma chave da AWS e confere que a varredura acha.
#!/usr/bin/env bash
# base-segura.sh - monta do zero um repositorio base seguro para um projeto
# escrito por IA.
#
# O script cria a arvore de arquivos, instala o gancho de commit que barra
# segredo, roda a suite de testes e a varredura de segredo, e prova que a
# trava funciona. O git e opcional: se a imagem nao tiver git, os arquivos
# sao criados mesmo assim e o script diz o que ficou para fora.
#
# bash base-segura.sh [nome-do-projeto] [responsavel]
#
# bash base-segura.sh api de-equipe
#
# Nada e instalado. O projeto nasce sem dependencia nenhuma, e por isso o
# portao roda em menos de dois segundos e nao tem cadeia de suprimento para
# atacar. O unico requisito e bash.
set -u
PROJETO="${1:-projeto}"
RESPONSAVEL="${2:-${GIT_OWNER:-desconhecido}}"
RAIZ="$(pwd)"
DESTINO="$RAIZ/$PROJETO"
tem_git() { command -v git >/dev/null 2>&1; }
echo "== 1. criando a arvore em $PROJETO =="
mkdir -p "$DESTINO"/src "$DESTINO"/tests "$DESTINO"/scripts "$DESTINO"/.githooks \
"$DESTINO"/.github/workflows "$DESTINO"/revisoes
# O git nao versiona pasta vazia. Sem este arquivo, a pasta revisoes/
# desaparece num checkout e o registro de revisao some junto.
: > "$DESTINO/revisoes/.gitkeep"
# --------------------------------------------------------------- AGENTS.md
cat > "$DESTINO/AGENTS.md" <<'AGENTS'
# AGENTS.md
Regras do repositorio para quem escreve codigo aqui, humano ou agente. Este
arquivo e lido no inicio de toda tarefa. Se uma regra aqui conflita com um
pedido no chat, o pedido do chat vence, e o certo e corrigir este arquivo
depois, nao furar a regra em silencio.
## O que este projeto e
Projeto de exemplo, com uma funcao em `src/saude.sh` e a suite em `tests/`.
O esqueleto e o que importa: cada modulo novo vem com o seu
`tests/<modulo>.test.sh`.
## Como rodar o portao
bash scripts/checar.sh
Esse comando e o portao. Ele roda a suite de testes e depois a varredura de
segredo, e sai com codigo 1 se qualquer uma das duas falhar. Rodar antes de
qualquer commit e antes de pedir revisao. Nao existe outro caminho para o
que nao passou por ele.
## Como adicionar um teste
1. Crie `tests/<modulo>.test.sh` com permissao de execucao.
2. O arquivo define funcoes `teste_*` e chama `assert_igual`, `assert_codigo`
e `finaliza`.
3. `scripts/testar.sh` descobre os arquivos, roda e conta.
4. Modulo novo sem teste correspondente nao entra.
## Regras que nao se quebram
- Nenhum segredo no repositorio. Credencial vai para variavel de ambiente, e o
arquivo de ambiente local esta no `.gitignore`.
- Nao se usa `git commit --no-verify`. O gancho de commit existe para barrar
credencial, e burlar ele e burrice, nao atalho.
- Nao se apaga nem se enfraquece `scripts/segredos.sh` para fazer o portao
passar. Se a varredura acusa um falso positivo, corrige a expressao
regular dela.
- Nao se mexe em `.github/workflows/ci.yml` para desligar etapa. Se a etapa
esta errada, conserta a etapa.
- Teste que falha e teste que nao existe tem o mesmo valor: zero. Mudanca de
codigo sem mudanca de teste volta para tras.
- Todo commit explica o porque, nao o o que. O diff ja mostra o que.
## Formato do commit
`<tipo>: <descricao curta no imperativo>`, com `tipo` entre `feat`, `fix`,
`test`, `docs`, `refactor` e `chore`. Uma linha, no maximo 72 colunas.
## O que o agente nao faz sem perguntar
- Apagar arquivo que tem teste.
- Mudar porta, rota ou nome de variavel de ambiente.
- Colocar dependencia nova no projeto. O projeto nasce sem dependencia, e
cada dependencia e uma superficie de ataque e um arquivo a mais para
atualizar.
AGENTS
# --------------------------------------------------------------- gitignore
cat > "$DESTINO/.gitignore" <<'IGNORADO'
# configuracao local e credencial: nunca entram no repositorio
.env
.env.*
*.pem
*.key
*.p12
*.pfx
credentials
credentials.*
secrets/
*_rsa
id_rsa
id_ed25519
# resultado de build e cache
node_modules/
dist/
build/
__pycache__/
*.pyc
.vendor/
vendor/
IGNORADO
# --------------------------------------------------------------- codigo
cat > "$DESTINO/src/saude.sh" <<'SAUDE'
#!/usr/bin/env bash
# src/saude.sh - o modulo de exemplo do projeto.
#
# Duas funcoes: uma que monta o alvo a partir de uma URL, que e pura e
# testavel sem rede, e outra que testa a conexao de verdade.
set -u
alvo_url() {
# alvo_url <url> -> imprime "host porta caminho"
local url="$1" resto host porta caminho
resto="${url#*://}"
host="${resto%%/*}"
porta="${host##*:}"
if [ "$porta" = "$host" ]; then porta=80; fi
host="${host%%:*}"
if [ "$resto" = "${resto#*/}" ]; then
caminho="/"
else
caminho="/${resto#*/}"
fi
# https sem porta explicita usa 443 no lugar de 80
if [ "$url" != "${url#https://}" ] && [ "$porta" = "80" ]; then porta=443; fi
printf '%s %s %s\n' "$host" "$porta" "$caminho"
}
saude() {
# saude <host> <porta> -> 0 quando responde, 1 quando nao
local alvo="${1:-localhost}"
local porta="${2:-8080}"
if timeout 3 bash -c "exec 3<>/dev/tcp/$alvo/$porta" 2>/dev/null; then
printf '%s:%s responde\n' "$alvo" "$porta"
return 0
fi
printf '%s:%s nao responde\n' "$alvo" "$porta" >&2
return 1
}
SAUDE
cat > "$DESTINO/tests/ajuda.sh" <<'AJUDA'
#!/usr/bin/env bash
# tests/ajuda.sh - as assercoes da suite.
#
# Cada arquivo de teste source este arquivo, define funcoes que comecam com
# teste_ e chama finaliza no fim. A contagem de falhas e o codigo de saida
# sao daqui.
set -u
TOTAL=0
FALHAS=0
assert_igual() {
# assert_igual <esperado> <obtido> <descricao>
TOTAL=$((TOTAL + 1))
if [ "$1" != "$2" ]; then
FALHAS=$((FALHAS + 1))
printf ' FALHOU: %s (esperado "%s", veio "%s")\n' "$3" "$1" "$2" >&2
fi
}
assert_codigo() {
# assert_codigo <esperado> <obtido> <descricao>
TOTAL=$((TOTAL + 1))
if [ "$1" != "$2" ]; then
FALHAS=$((FALHAS + 1))
printf ' FALHOU: %s (esperado codigo %s, veio %s)\n' "$3" "$1" "$2" >&2
fi
}
finaliza() {
printf ' %d verificacao(oes), %d falha(s)\n' "$TOTAL" "$FALHAS"
[ "$FALHAS" -eq 0 ] || exit 1
exit 0
}
AJUDA
cat > "$DESTINO/tests/saude.test.sh" <<'TESTE'
#!/usr/bin/env bash
# tests/saude.test.sh - teste do modulo src/saude.sh
#
# A maior parte dos testes nao usa rede. O teste que precisa de um servidor
# no ar so roda quando ha python3 na maquina; sem ele, o teste e pulado e
# isso aparece no placar.
set -u
DIRETORIO="$(cd "$(dirname "$0")" && pwd)"
. "$DIRETORIO/ajuda.sh"
. "$DIRETORIO/../src/saude.sh"
teste_url_com_http() {
saida=$(alvo_url "http://exemplo.org/saude")
assert_igual "exemplo.org 80 /saude" "$saida" "url http vira host, porta e caminho"
}
teste_url_com_https() {
saida=$(alvo_url "https://exemplo.org")
assert_igual "exemplo.org 443 /" "$saida" "url https sem caminho usa porta 443"
}
teste_url_com_porta_explicita() {
saida=$(alvo_url "http://exemplo.org:8080/api")
assert_igual "exemplo.org 8080 /api" "$saida" "porta explicita e preservada"
}
teste_porta_fechada() {
saida=$(saude 127.0.0.1 9 2>&1)
codigo=$?
assert_codigo 1 "$codigo" "porta fechada devolve codigo 1"
assert_igual "127.0.0.1:9 nao responde" "$saida" "mensagem de porta fechada"
}
teste_porta_aberta() {
if ! command -v python3 >/dev/null 2>&1; then
printf ' pulado: sem python3 nesta maquina\n'
return 0
fi
python3 -c '
import http.server, threading, time
s = http.server.HTTPServer(("127.0.0.1", 18099), http.server.SimpleHTTPRequestHandler)
threading.Thread(target=s.serve_forever, daemon=True).start()
time.sleep(8)
' &
PID=$!
sleep 1
saida=$(saude 127.0.0.1 18099 2>&1)
codigo=$?
kill "$PID" 2>/dev/null
wait "$PID" 2>/dev/null
assert_codigo 0 "$codigo" "porta aberta devolve codigo 0"
assert_igual "127.0.0.1:18099 responde" "$saida" "mensagem de porta aberta"
}
teste_url_com_http
teste_url_com_https
teste_url_com_porta_explicita
teste_porta_fechada
teste_porta_aberta
finaliza
TESTE
# --------------------------------------------------------------- varredura
cat > "$DESTINO/scripts/segredos.sh" <<'SEGREDOS'
#!/usr/bin/env bash
# scripts/segredos.sh - varre o repositorio procurando credencial.
#
# bash scripts/segredos.sh [caminho...]
#
# Sem argumento, varre o diretorio do projeto inteiro. Com argumento, varre
# so o que foi indicado. Sai com codigo 0 quando nao achou nada e com codigo
# 1 quando achou.
#
# As expressoes regulares sao as regras. Cada linha do bloco PADROES tem o
# nome do servico, dois pontos, e a expressao. O que a expressao casa e o
# motivo de o arquivo estar na lista de ignorados.
set -u
# Arquivos que a varredura nao olha. O proprio scanner entra aqui, porque as
# expressoes regulares dele seriam detectadas como credencial.
IGNORADOS="scripts/segredos.sh"
PADROES=$(cat <<'FIM'
chave-privada:-----BEGIN [A-Z ]*PRIVATE KEY-----
aws-access-key:AKIA[0-9A-Z]{16}
aws-secret-key:aws_secret_access_key[[:space:]]*=[[:space:]]*[A-Za-z0-9/+=]{40}
github-token:gh[pousr]_[A-Za-z0-9]{36}
github-fine-grained:github_pat_[A-Za-z0-9_]{50,}
openai-key:sk-[A-Za-z0-9]{20,}
anthropic-key:sk-ant-[A-Za-z0-9-]{20,}
google-api-key:AIza[0-9A-Za-z_-]{35}
slack-token:xox[baprs]-[A-Za-z0-9-]{10,}
slack-webhook:hooks\.slack\.com/services/[A-Za-z0-9/]+
stripe-key:[sr]k_(live|test)_[A-Za-z0-9]{20,}
telegram-token:[0-9]{8,10}:[A-Za-z0-9_-]{35}
jwt:eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}
atribuicao-generica:(senha|password|passwd|api[_-]?key|secret|token)[[:space:]]*[:=][[:space:]]*["']?[A-Za-z0-9/+_.-]{16,}
FIM
)
alvo_principal() {
# o diretorio do projeto, que e o pai de scripts/
cd "$(dirname "$0")/.." && pwd
}
ignorado() {
# ignorado <caminho> -> 0 quando o arquivo nao deve ser lido
local arquivo="$1" ig
for ig in $IGNORADOS; do
case "$arquivo" in
"$ig"|*/"$ig") return 0 ;;
esac
done
return 1
}
procurar() {
# devolve 0 quando achou credencial, 1 quando nao achou
local raiz="$1" linha nome padrao arquivo achou=1
while IFS= read -r linha; do
[ -n "$linha" ] || continue
nome="${linha%%:*}"
padrao="${linha#*:}"
[ -n "$padrao" ] || continue
while IFS= read -r arquivo; do
[ -n "$arquivo" ] || continue
ignorado "$arquivo" && continue
if grep -Eq "$padrao" "$arquivo" 2>/dev/null; then
printf 'CREDENCIAL [%s] em %s (linha %s)\n' "$nome" "$arquivo" \
"$(grep -En "$padrao" "$arquivo" 2>/dev/null | head -1 | cut -d: -f1)" >&2
achou=0
fi
done < <(find "$raiz" -type f -not -path "*/.git/*" -not -path "*/revisoes/*" 2>/dev/null | sort)
done <<< "$PADROES"
return $achou
}
if [ "$#" -gt 0 ]; then
ALVO="$1"
else
ALVO="$(alvo_principal)"
fi
echo "varredura de credencial em $ALVO"
if procurar "$ALVO"; then
echo "varredura: credencial encontrada, o commit foi barrado" >&2
exit 1
fi
echo "varredura: nenhuma credencial encontrada"
exit 0
SEGREDOS
# --------------------------------------------------------------- testes
cat > "$DESTINO/scripts/testar.sh" <<'TESTAR'
#!/usr/bin/env bash
# scripts/testar.sh - roda todos os arquivos tests/*.test.sh e conta.
set -u
DIRETORIO="$(cd "$(dirname "$0")/.." && pwd)"
cd "$DIRETORIO" || exit 1
TOTAL_ARQUIVOS=0
TOTAL_FALHAS=0
for arquivo in tests/*.test.sh; do
[ -e "$arquivo" ] || continue
TOTAL_ARQUIVOS=$((TOTAL_ARQUIVOS + 1))
echo "--- $arquivo"
bash "$arquivo" || TOTAL_FALHAS=$((TOTAL_FALHAS + 1))
done
if [ "$TOTAL_ARQUIVOS" -eq 0 ]; then
echo "nenhum arquivo de teste em tests/" >&2
exit 1
fi
echo "testes: $TOTAL_ARQUIVOS arquivo(s), $TOTAL_FALHAS com falha"
[ "$TOTAL_FALHAS" -eq 0 ] || exit 1
exit 0
TESTAR
cat > "$DESTINO/scripts/checar.sh" <<'CHECAR'
#!/usr/bin/env bash
# scripts/checar.sh - o portao. Roda os testes e a varredura de credencial.
#
# bash scripts/checar.sh
#
# Sai com codigo 0 quando os dois passaram, e com codigo 1 quando algum
# falhou. E este comando que a CI roda e que o gancho de commit chama.
set -u
DIRETORIO="$(cd "$(dirname "$0")/.." && pwd)"
cd "$DIRETORIO" || exit 1
echo "== testes =="
bash scripts/testar.sh
TESTES=$?
echo
echo "== varredura de credencial =="
bash scripts/segredos.sh
SEGREDOS=$?
echo
if [ "$TESTES" -eq 0 ] && [ "$SEGREDOS" -eq 0 ]; then
echo "portao: aprovado"
exit 0
fi
echo "portao: reprovado (testes=$TESTES, varredura=$SEGREDOS)" >&2
exit 1
CHECAR
# --------------------------------------------------------------- merge
cat > "$DESTINO/scripts/merge.sh" <<'MERGE'
#!/usr/bin/env bash
# scripts/merge.sh - a trava de revisao antes de mesclar.
#
# bash scripts/merge.sh <branch> [--executar]
#
# O script so libera o merge quando as tres condicoes valem ao mesmo tempo:
# o portao passa, existe revisao registrada para o commit atual da branch, e
# quem revisou nao e o proprio autor do commit. Sem --executar ele apenas
# diz o que falta.
set -u
BRANCHE="${1:-}"
EXECUTAR="${2:-}"
DIRETORIO="$(cd "$(dirname "$0")/.." && pwd)"
cd "$DIRETORIO" || exit 1
if [ -z "$BRANCHE" ]; then
echo "uso: bash scripts/merge.sh <branch> [--executar]" >&2
exit 2
fi
falha=0
echo "== 1. portao =="
if bash scripts/checar.sh >/dev/null 2>&1; then
echo "ok: testes e varredura passaram"
else
echo "FALHOU: o portao reprovou" >&2
falha=1
fi
echo
echo "== 2. revisao registrada =="
COMMIT="$(git rev-parse "$BRANCHE" 2>/dev/null)"
if [ -z "$COMMIT" ]; then
echo "FALHOU: branch $BRANCHE nao existe" >&2
falha=1
else
ARQUIVO="revisoes/$BRANCHE.txt"
if [ ! -f "$ARQUIVO" ]; then
echo "FALHOU: nao ha revisao em $ARQUIVO" >&2
echo " o revisor cria o arquivo com: revisor <commit> <autor-do-commit>" >&2
falha=1
else
REVISOR="$(awk 'NR==1 {print $1}' "$ARQUIVO")"
REVISADO="$(awk 'NR==1 {print $2}' "$ARQUIVO")"
AUTOR="$(git log -1 --format='%an' "$COMMIT" 2>/dev/null)"
if [ "$REVISADO" != "$COMMIT" ]; then
echo "FALHOU: a revisao e do commit $REVISADO, a branch esta em $COMMIT" >&2
falha=1
elif [ "$REVISOR" = "$AUTOR" ]; then
echo "FALHOU: quem revisou ($REVISOR) e o autor do commit" >&2
falha=1
else
echo "ok: $REVISOR revisou o commit $COMMIT"
fi
fi
fi
echo
if [ "$falha" -ne 0 ]; then
echo "merge: barrado" >&2
exit 1
fi
echo "merge: liberado"
if [ "$EXECUTAR" = "--executar" ]; then
git merge --no-ff "$BRANCHE" -m "merge de $BRANCHE"
echo "merge executado"
fi
exit 0
MERGE
# --------------------------------------------------------------- gancho
cat > "$DESTINO/.githooks/pre-commit" <<'GANCHO'
#!/usr/bin/env bash
# .githooks/pre-commit - barra credencial antes do commit.
#
# O git so chama este gancho se core.hooksPath apontar para .githooks. O
# script base-segura.sh faz isso na hora de instalar.
set -u
DIRETORIO="$(cd "$(dirname "$0")/.." && pwd)"
if bash "$DIRETORIO/scripts/segredos.sh" >/dev/null 2>&1; then
exit 0
fi
echo "COMMIT BARRADO: credencial encontrada no que seria commitado." >&2
echo "Rode 'bash scripts/segredos.sh' para ver onde esta." >&2
echo "Nao use --no-verify. A credencial vai para variavel de ambiente." >&2
exit 1
GANCHO
# O git so executa o gancho se ele for executavel. Sem o chmod, o git avisa
# que ignorou o gancho e o commit passa sem passar pela varredura.
chmod +x "$DESTINO/.githooks/pre-commit" \
"$DESTINO"/scripts/*.sh \
"$DESTINO"/tests/*.sh \
"$DESTINO"/src/*.sh
# --------------------------------------------------------------- CI
cat > "$DESTINO/.github/workflows/ci.yml" <<'CI'
# .github/workflows/ci.yml - a CI do projeto.
#
# Dois jobs, e os dois precisam passar:
# portao roda os testes e a varredura de credencial
# revisao num pull request, exige revisao aprovada de outra pessoa
#
# O nome do job e o que a protecao de branch exige. Se voce renomear o job,
# renomeie tambem na configuracao da branch, senao a regra fica apontando
# para um status check que nao existe.
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, submitted]
permissions:
contents: read
jobs:
portao:
name: portao
runs-on: ubuntu-latest
steps:
- name: pega o codigo
uses: actions/checkout@v4
- name: roda os testes e a varredura de credencial
run: bash scripts/checar.sh
- name: mostra o que foi verificado
run: ls -la scripts/ tests/ AGENTS.md .github/workflows/ci.yml
revisao:
name: revisao aprovada
runs-on: ubuntu-latest
# num push direto na main nao existe pull request, e nao ha o que exigir
if: github.event_name == 'pull_request'
steps:
- name: exige revisao de outra pessoa
env:
ACAO: ${{ github.event.action }}
ESTADO: ${{ github.event.review.state }}
AUTOR: ${{ github.event.pull_request.user.login }}
REVISOR: ${{ github.event.review.user.login }}
run: |
if [ "$ACAO" != "submitted" ]; then
echo "ainda nao ha revisao para este pull request"
exit 1
fi
if [ "$ESTADO" != "approved" ]; then
echo "revisao nao aprovada (estado: $ESTADO)"
exit 1
fi
if [ "$REVISOR" = "$AUTOR" ]; then
echo "quem revisou e o proprio autor do pull request"
exit 1
fi
echo "revisao aprovada por $REVISOR"
CI
# --------------------------------------------------------------- README
cat > "$DESTINO/README.md" <<'LEIA'
# $PROJETO
Projeto de exemplo, montado pelo `base-segura.sh`.
## Antes de qualquer coisa
bash scripts/checar.sh
Esse e o portao. Testes, varredura de credencial, e saida 1 quando algo falha.
## Estrutura
AGENTS.md as regras que o agente e o humano seguem
.githooks/ o gancho de commit que barra credencial
.github/workflows/ a CI
scripts/ portao, suite de testes e varredura
src/ o codigo
tests/ um arquivo de teste por modulo
revisoes/ registro de revisao, um arquivo por branch
## Regra de ouro
O portao decide o que entra. Se ele reprovou, o que falta esta na saida dele.
LEIA
# --------------------------------------------------------------- git
if tem_git; then
echo
echo "== 2. instalando o repositorio e o gancho de commit =="
cd "$DESTINO" || exit 1
git init -q -b main
git config core.hooksPath .githooks
git config user.name "$RESPONSAVEL"
git config user.email "$RESPONSAVEL@localhost"
git add -A
git -c commit.gpgsign=false commit -q -m "chore: esqueleto do projeto com portao, varredura e gancho"
echo "repositorio criado, gancho instalado (core.hooksPath = .githooks)"
echo "commits: $(git rev-list --count HEAD)"
else
echo
echo "== 2. git nao esta nesta imagem: os arquivos foram criados, o repo nao =="
echo " git init -b main"
echo " git config core.hooksPath .githooks"
echo " git add -A && git commit -m 'chore: esqueleto do projeto'"
fi
# --------------------------------------------------------------- prova
echo
echo "== 3. rodando o portao =="
cd "$DESTINO" || exit 1
bash scripts/checar.sh
PORTACAO=$?
echo
echo "== 4. provando que a trava de credencial funciona =="
PASTA_PROVA="$(mktemp -d)"
cat > "$PASTA_PROVA/vazamento.txt" <<'VAZAMENTO'
# arquivo de teste: simula quem deixou um token da AWS no codigo
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
VAZAMENTO
echo "arquivo de mentira criado em $PASTA_PROVA/vazamento.txt"
if bash scripts/segredos.sh "$PASTA_PROVA" >/dev/null 2>&1; then
echo "ERRO: a varredura NAO achou a credencial de mentira"
PROVA_FALHOU=1
else
echo "a varredura achou a credencial de mentira, como deve:"
bash scripts/segredos.sh "$PASTA_PROVA" 2>&1 | sed 's/^/ /'
PROVA_FALHOU=0
fi
rm -rf "$PASTA_PROVA"
echo
echo "== 5. arvore criada =="
cd "$DESTINO" || exit 1
find . -type f -not -path "./.git/*" | sort | sed 's/^/ /'
echo
echo "resumo: portao=$PORTACAO, trava=$PROVA_FALHOU"
if [ "$PORTACAO" -eq 0 ] && [ "$PROVA_FALHOU" -eq 0 ]; then
echo "projeto pronto"
exit 0
fi
exit 1
Como verificar
Rode o script em uma pasta vazia.
bash base-segura.sh api de-equipe
== 3. rodando o portao ==
--- tests/saude.test.sh
7 verificacao(oes), 0 falha(s)
testes: 1 arquivo(s), 0 com falha
== varredura de credencial ==
varredura: nenhuma credencial encontrada
portao: aprovado
== 4. provando que a trava de credencial funciona ==
a varredura achou a credencial de mentira, como deve:
CREDENCIAL [aws-access-key] em /tmp/tmp.aq2UIfopD9/vazamento.txt (linha 2)
resumo: portao=0, trava=0
projeto pronto
O portão leva 1,6 segundos.
O gancho barrando um arquivo com credencial de verdade aparece no teste completo:
COMMIT BARRADO: credencial encontrada no que seria commitado.
Nao use --no-verify. A credencial vai para variavel de ambiente.
E a trava de merge barrando nas três situações: sem revisão registrada, com revisão do próprio autor, e liberada só com revisor de outra pessoa.
Se der errado
O commit passa com credencial dentro e o git avisa que ignorou o gancho. Causa: o gancho não é executável, ou core.hooksPath não aponta para .githooks. Correção: chmod +x .githooks/pre-commit e git config core.hooksPath .githooks.
A varredura acusa o próprio scanner. Causa: as expressões regulares dele estão num arquivo que ele varre. Correção: o script já ignora scripts/segredos.sh, e a lista IGNORADOS no topo é onde se acrescentar exclusões.
A branch não é mesclada mesmo com o portão verde. Causa: a revisão é de um commit anterior e a branch andou. Correção: a trava compara o commit revisado com o HEAD da branch.
Tabela
| Peça | O que segura | Custo | Quando usar |
|---|---|---|---|
| AGENTS.md | regra que o agente respeita | zero, um arquivo | sempre |
| gancho de commit | credencial no commit local | zero, bash | sempre |
| varredura em CI | credencial no histórico | runners grátis do GitHub | repo público ou plano pago |
| gitleaks | o mesmo, com regras prontas | zero, open source | quando não quero manter a lista |
| branch protegida | merge sem teste e sem revisão | zero no repo público | branch principal |
FAQ
Por que o AGENTS.md e não um README? O README é para gente: start rápido, descrição do projeto. O AGENTS.md é o contexto extra que o agente precisa, e que polui o README de quem só quer usar o projeto.
Preciso de uma conta paga do GitHub? Não. Repo público tem secret scanning e branch protegida de graça. Só o Secret Protection em repo privado de organização exige plano pago.
A revisão humana não é o gargalo? É, e é o ponto. A trava pede revisão de cada mudança, não de cada linha. Quem etiqueta o próprio pull request é quem ela barra.
E se o agente pedir para desligar a trava? O AGENTS.md diz que não, e o gancho não obedece a ninguém. Para credencial temporária, o caminho é variável de ambiente mais .env no gitignore.
Fontes
A página do AGENTS.md, do Agentic AI Foundation, conferida em 20 out 2026. A documentação de branch protegida e a documentação de secret scanning, do GitHub Docs. O manual de githooks, do git-scm.
O método de medir antes de concluir está no conjunto de 20 perguntas conferíveis. E o lado de credencial em repositório está no o que a senha 123456 custou.