MÓDULO 1.4

🧠 Estado e Persistência: Memória entre Sessões

📚

Tópicos

6

⏱️

Minutos

45

🎯

Nível

Iniciante+

🔒

Tipo

Técnico

1

🧠 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
2

📄 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
3

⚛️ 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).

4

🔒 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
5

🔐 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).

6

📖 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

1.yq v4+ — parse correto de YAML com tipos, suporte a arrays e objetos aninhados
2.jq — parse JSON robusto, queries complexas, transformações in-place
3.grep + sed — fallback frágil, funciona apenas para campos simples na raiz

Sempre trate ausência de ferramenta graciosamente — use defaults e não quebre o hook.

Resumo do Módulo 1.4

Sessões são stateless — use arquivos para persistir estado entre execuções
YAML para humanos, JSON para código — escolha conforme o consumidor primário
Escrita atômica com mv — tmpfile + mv no mesmo filesystem = nunca corrompido
CAS para concorrência otimista — lê, verifica, escreve — retenta se conflito
Lockfile para exclusividade — mkdir atômico com trap EXIT para cleanup garantido
yq/jq com fallback — sempre trate ausência de dependência graciosamente

Próximo Módulo:

1.5 — O Ambiente para Construir — estrutura de repo, testes isolados e o kit de ferramentas completo