🧠 O problema — sem memória entre sessões
Cada sessão do Claude Code começa do zero. O modelo não lembra da sessão anterior — nem dos arquivos que leu, nem das decisões que tomou, nem do que você configurou no contexto. Para criar sistemas que evoluem ao longo do tempo, você precisa externalizar o estado.
⚠️ O que NÃO persiste entre sessões
- ✗Variáveis de ambiente definidas durante a sessão
- ✗O histórico de conversa (context window)
- ✗Qualquer estado em memória dos hooks
- ✗Preferências ou aprendizados da sessão anterior
✓ O que PERSISTE entre sessões
- ✓Arquivos no filesystem — qualquer arquivo que o Claude escreve persiste
- ✓CLAUDE.md — instruções lidas no início de cada sessão
- ✓settings.json — hooks e comandos registrados
- ✓Arquivos de estado — YAML/JSON escritos pelos hooks
📄 Estado em arquivo: YAML e JSON como memória
O padrão para persistir estado em hooks é usar arquivos YAML ou JSON como banco de dados simples. YAML é preferido quando humanos precisam ler/editar o arquivo. JSON quando o dado é consumido programaticamente.
📄 Exemplo YAML
version: 1
updated_at: "2026-01-20T14:30:00Z"
hook: validate-commit
stats:
total_runs: 47
blocked: 3
approved: 44
last_blocked:
command: "git commit -m 'fix'"
reason: "Mensagem muito curta"
at: "2026-01-20T14:25:00Z"
📋 Exemplo JSON
{
"version": 1,
"updated_at": "2026-01-20T14:30:00Z",
"hook": "validate-commit",
"stats": {
"total_runs": 47,
"blocked": 3,
"approved": 44
}
}
📊 Estrutura recomendada de estado
Todo arquivo de estado deve ter pelo menos:
- •version: número inteiro para migração de schema futuro
- •updated_at: timestamp ISO 8601 da última escrita
- •hook: nome do hook responsável pelo arquivo
- •data: o estado de negócio em si
⚛️ Escrita atômica — por que mv é melhor
Escrever diretamente em um arquivo é perigoso: se o processo for interrompido no meio, o arquivo fica corrompido — metade do conteúdo antigo, metade do novo. A solução é escrita atômica: escreva em arquivo temporário, depois use mv.
✗ Escrita direta (perigoso)
# PERIGOSO: se interrompido aqui,
# o arquivo fica corrompido
echo "$NOVO_CONTEUDO" > state.yaml
# ^ pode falhar no meio
Se o processo morrer no meio da escrita, state.yaml fica vazio ou parcial.
✓ Escrita atômica (seguro)
# SEGURO: escreve no temporário
TMPFILE=$(mktemp)
echo "$NOVO_CONTEUDO" > "$TMPFILE"
# Depois move atomicamente
mv "$TMPFILE" state.yaml
# ^ atomic no mesmo filesystem
mv é uma syscall rename() — atômica no kernel. Nunca estado parcial.
💡 Condição de atomicidade
mv só é atômico quando origem e destino estão no mesmo filesystem. Por isso use mktemp no mesmo diretório: TMPFILE=$(mktemp .claude/state/state.XXXXXX.yaml).
🔒 Compare-and-swap (CAS) — evitando race conditions
Quando múltiplos hooks podem rodar em paralelo, existe risco de um sobrescrever o estado do outro. Compare-and-swap (CAS) é o padrão para evitar isso: lê o estado, verifica se ainda é o esperado antes de escrever.
🔄 Implementação de CAS em shell
#!/bin/bash
# Padrão CAS para atualização segura de estado
STATE_FILE=".claude/state/hook-state.yaml"
MAX_RETRIES=3
RETRY_DELAY=0.1
for i in $(seq 1 $MAX_RETRIES); do
# 1. Lê o estado atual
CURRENT_VERSION=$(yq '.version' "$STATE_FILE" 2>/dev/null || echo "0")
# 2. Calcula o novo estado
NEW_VERSION=$((CURRENT_VERSION + 1))
NOVO_CONTEUDO="version: $NEW_VERSION
updated_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)
runs: $(yq '.runs // 0' "$STATE_FILE" | awk '{print $1+1}')"
# 3. Escreve no temporário
TMPFILE=$(mktemp ".claude/state/state.XXXXXX.yaml")
echo "$NOVO_CONTEUDO" > "$TMPFILE"
# 4. Verifica se a versão ainda é a mesma (CAS)
CURRENT_AT_WRITE=$(yq '.version' "$STATE_FILE" 2>/dev/null || echo "0")
if [ "$CURRENT_AT_WRITE" = "$CURRENT_VERSION" ]; then
# Versão não mudou: pode escrever
mv "$TMPFILE" "$STATE_FILE"
exit 0
else
# Conflito: outra instância atualizou. Retenta.
rm -f "$TMPFILE"
sleep $RETRY_DELAY
fi
done
echo "CAS falhou após $MAX_RETRIES tentativas" >&2
exit 0 # fail-open
🔐 Lockfiles — execução exclusiva
Para operações que absolutamente não podem ser paralelas, lockfiles garantem exclusividade. A técnica usa mkdir como operação atômica para criar um "semáforo" no filesystem.
🔐 Implementação de lockfile
#!/bin/bash
# Lockfile com cleanup automático
LOCK_DIR=".claude/state/hook.lock"
MAX_WAIT=5 # segundos
# Cleanup no EXIT (mesmo em caso de erro)
cleanup() {
rmdir "$LOCK_DIR" 2>/dev/null || true
}
trap cleanup EXIT
# Tenta adquirir o lock (mkdir é atômico)
WAITED=0
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
# Verifica lock stale (mais de 30s)
if [ -d "$LOCK_DIR" ]; then
LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$LOCK_DIR" 2>/dev/null || echo 0) ))
if [ "$LOCK_AGE" -gt 30 ]; then
rmdir "$LOCK_DIR" 2>/dev/null || true
continue
fi
fi
if [ "$WAITED" -ge "$MAX_WAIT" ]; then
echo "Lock timeout após ${MAX_WAIT}s — fail-open" >&2
exit 0 # fail-open
fi
sleep 0.5
WAITED=$((WAITED + 1))
done
# --- Seção crítica (execução exclusiva) ---
echo $$ > "$LOCK_DIR/pid"
# ... operação crítica aqui ...
# --- Fim da seção crítica ---
# cleanup() remove o lock automaticamente via trap EXIT
💡 Quando usar lockfile vs CAS
Use CAS para atualizações de estado (operações curtas, retentável). Use lockfile para operações longas ou que envolvem múltiplos arquivos (onde você não pode simplesmente retentar do início).
📖 Lendo estado no hook — yq e jq
Ler estado eficientemente no caminho crítico de um hook requer as ferramentas certas. yq para YAML, jq para JSON — com grep como fallback quando as ferramentas não estão disponíveis.
💻 Padrões de leitura
#!/bin/bash
STATE_FILE=".claude/state/hook-state.yaml"
# --- Leitura com yq (YAML) ---
VERSION=$(yq '.version // 0' "$STATE_FILE" 2>/dev/null || echo "0")
LAST_RUN=$(yq '.last_run // ""' "$STATE_FILE" 2>/dev/null || echo "")
# --- Leitura com jq (JSON) ---
JSON_FILE=".claude/state/hook-state.json"
RUNS=$(jq -r '.stats.total_runs // 0' "$JSON_FILE" 2>/dev/null || echo "0")
# --- Fallback com grep (sem dependências) ---
# Funciona para YAML e JSON simples
VERSION_GREP=$(grep -m1 '"version"' "$JSON_FILE" 2>/dev/null | grep -o '[0-9]*' || echo "0")
# --- Verificação de existência antes de ler ---
if [ ! -f "$STATE_FILE" ]; then
echo "Estado não encontrado — usando defaults"
VERSION=0
LAST_RUN=""
fi
# --- Validação de tipo ---
if ! [[ "$VERSION" =~ ^[0-9]+$ ]]; then
echo "Versão inválida no estado: '$VERSION' — usando 0" >&2
VERSION=0
fi
📊 Hierarquia de dependências
Sempre trate ausência de ferramenta graciosamente — use defaults e não quebre o hook.
✅ Resumo do Módulo 1.4
Próximo Módulo:
1.5 — O Ambiente para Construir — estrutura de repo, testes isolados e o kit de ferramentas completo