>_ josetilabs.com
← blog

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.

Gráfico de barras com as 523 linhas do esqueleto do projeto divididas em quatro grupos: código do projeto 132 linhas, execução dos scripts e do gancho 224 linhas, workflow da CI 61 linhas e documentação 84 linhas
Só um quarto do esqueleto é código. O resto é o que impede o código de entrar sem passar por teste e por outra pessoa.

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.

bash
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.

bash
#!/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
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.

Leia também

// Por e-mail

Os textos publicados aqui também vão por e-mail. O envio ainda não começou: quem deixar o endereço agora entra na primeira lista.

Guardo só o seu e-mail, a data e o registro desta autorização. Sem cookies, sem rastreador e nada compartilhado com terceiros. Detalhes na política de privacidade.