Free

Portando o session recording do Claude Code para um segundo projeto

Sistema de gravação do how2claude pra outro projeto Rails: 4 arquivos, 5 passos, camadas de hook.


how2claude tem um conjunto de hooks que auto-gravam as sessions do Claude Code — começa a gravação quando o trabalho arranca, anexa um checkpoint em cada commit e, no fim da session, extrai prompts / bash / lista de edits pro docs/notes/<feature>/raw.md. O artigo let-claude-record-itself cobriu como foi montado.

Problema: meu outro projeto (smarts, um site de docs pra smart contracts) não tinha nada disso. Toda vez que eu queria escrever um artigo depois, acabava escarafunchando git log e a memória, sentindo que tava perdendo o bom. Esse artigo conta como portei o sistema de gravação — 4 arquivos, 5 minutos no total — mas no caminho apareceu uma sacada real sobre camadas de hook: hooks do Claude Code e hooks do git operam em camadas completamente diferentes, e o que é capturado quando você mistura ferramentas (Amp + Claude Code, por exemplo) depende de em qual camada você instalou.


4 arquivos, uma figura

Tudo do how2claude relacionado à gravação:

Arquivo Camada Papel
bin/recording-state script helper Python, gerencia o ciclo de vida do .state.json
bin/extract-session-notes script helper Python, lê o transcript do Claude Code → escreve raw.md
.claude/settings.local.json hook do Claude Code PostToolUse / Stop disparam os scripts acima
.git/hooks/post-commit hook do git cada commit chama recording-state commit pra um checkpoint
.gitignore controle de ruído mantém docs/notes/ fora do repo (notas são privadas/transitórias)

4 peças, 4 camadas diferentes. Essa distinção volta abaixo.

A sacada chave: layering de hook decide o que é capturado

Dois sistemas de hook rodando ao mesmo tempo, com escopos bem diferentes:

Hooks do Claude Code (definidos em .claude/settings.local.json):
- Escopo: só dispara dentro da ferramenta Claude Code
- Disparadores: PostToolUse / Stop / PreToolUse — eventos do ciclo de vida do Claude Code
- Info disponível: nome da tool, args, transcript_path (jsonl completo da session) — coisas que só Claude Code sabe

Hooks do git (scripts shell em .git/hooks/):
- Escopo: dispara em todo evento do git, sem importar quem disparou o git
- Disparadores: post-commit / pre-push / etc.
- Info disponível: o que o próprio git sabe (sha, autor, branch, diff)

Consequência real: escrever código + commit dentro do Claude Code dispara as duas camadas — info da session e info do commit aterrissam no raw.md. Trocar pro Amp (ou Cursor, ou digitar à mão) pra escrever + commit e só o hook do git dispara — raw.md recebe o esqueleto do commit mas nenhum prompt / bash / detalhe de edit da session.

Não é bug — é a restrição de design de cada ferramenta. Querer detalhe em nível de session sob qualquer ferramenta significa instalar sua própria camada de hook por ferramenta. O fallback do git te dá o "o que foi feito"; não te dá o "o que foi pensado, o que quebrou".

Guia de seleção:
- Esqueleto agnóstico de ferramenta (info de commit, mudanças de código) → põe num hook de git
- Carne específica do Claude Code (prompts completos, raciocínio) → põe num hook do Claude Code
- Os dois → instala em ambas as camadas

Portar em 5 movimentos

Instalando o mesmo setup de gravação no smarts (/home/bob/Work/smarts, um projeto Rails).

1. Copiar os dois scripts Python

mkdir -p /home/bob/Work/smarts/bin
cp /home/bob/Work/how2claude/bin/recording-state \
   /home/bob/Work/smarts/bin/recording-state
cp /home/bob/Work/how2claude/bin/extract-session-notes \
   /home/bob/Work/smarts/bin/extract-session-notes
chmod +x /home/bob/Work/smarts/bin/{recording-state,extract-session-notes}

Scripts sem modificação — usam a env var $CLAUDE_PROJECT_DIR pra decidir onde escrever:

def project_dir():
    return os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()

def state_path():
    return pathlib.Path(project_dir()) / "docs/notes/.state.json"

Essa é a abstração chave: Claude Code seta CLAUDE_PROJECT_DIR automaticamente ao disparar hooks; a gente seta na mão dentro do post-commit do git. Os dois lados honram a mesma env var, e o script não precisa descobrir "em qual projeto eu tô".

2. Criar .claude/settings.local.json

Uma decisão aqui: portar só os hooks, não a lista de permissions.

O settings.local.json do how2claude tem mais de 100 entradas de permissions.allow — todas específicas do how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Zero sentido levar isso pro smarts. smarts vai acumular suas próprias permissions organicamente conforme for usado.

Hooks são padrões — idênticos entre projetos. Permissions são estado de projeto — diferentes entre projetos.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }
        ]
      }
    ]
  }
}

Três pontos de disparo:
- Arquivo editado → tenta arrancar gravação (maybe-start se auto-checa: se já tá gravando, pula; se tree limpo, também pula)
- Comando bash rodado → tenta parar (maybe-stop é rigoroso: só para se "auto-arrancado E de volta no master E tree limpo" valem ao mesmo tempo)
- Session termina → extrai o transcript pro raw.md

3. Criar .git/hooks/post-commit

3 linhas:

#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true

O export manual CLAUDE_PROJECT_DIR=$ROOT é o ponto exato onde o mundo do git e o mundo do Claude Code ficam conectados pela mesma env var. O || true garante que o hook nunca bloqueie um commit.

chmod +x /home/bob/Work/smarts/.git/hooks/post-commit

4. Adicionar docs/notes/ ao .gitignore

# Session recording notes (transient, for article material)
docs/notes/

Notas são transitórias + privadas — você não quer raw.md comitado em PRs; não quer .state.json poluindo git status. O gitignore do how2claude trata igual.

5. Smoke test (com uma surpresa pequena)

Disparar maybe-start manualmente logo após instalar:

$ cd /home/bob/Work/smarts && CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start
[recording] auto-started: contract-to-docs (branch: feat/contract-to-docs)

$ cat docs/notes/.state.json
{
  "feature": "contract-to-docs",
  "started_at": "2026-04-20T17:47:18-04:00",
  "branch": "feat/contract-to-docs",
  "auto_started": true
}

O script sacou corretamente que smarts por acaso tava em feat/contract-to-docs com tree sujo — auto-arrancou gravação, inferiu o nome de feature contract-to-docs da branch feat/contract-to-docs. Essa lógica no script:

def branch_to_feature(branch):
    if not branch or branch in ("master", "main"):
        return None
    if "/" in branch:
        return branch.split("/", 1)[1]
    return branch

feat/XX, feature/XX, fix-y puro → fix-y, master/main → None (que cai num nome com timestamp session-YYYYMMDD-HHMM).

A heurística é deliberadamente descerebrada — o nome da branch é o assunto do trabalho, não faz falta você nomear de novo.

Fronteira de visibilidade misturando com Amp

Depois de instalar me perguntei: às vezes trabalho no Amp — isso fica capturado? A resposta é exatamente o que a história do layering de hook prevê:

Cenário Hook Claude Code Hook git O que aterrissa nas notas
Trabalho Claude Code + commit ✅ dispara ✅ dispara detalhe de session + esqueleto de commit
Trabalho Amp + commit ❌ no-op ✅ dispara só esqueleto de commit
Digitação manual + commit ❌ no-op ✅ dispara só esqueleto de commit
Trabalho Claude Code, sem commit ainda ✅ arranca gravação detalhe de session (entrada de commit espera o próximo commit)

Conclusão: dá pro gasto se Claude Code for o motorista principal. Esqueleto de commit aterrissa sempre; detalhe de session aterrissa só na rota Claude Code. Pra artigos "o que foi feito" você apoia principalmente no body do commit — os prompts / rastros bash da session são bônus, bom ter mas não obrigatório.

Se você usa Amp pesado, Amp tem seu próprio sistema de hook (não entrei em detalhes); um script pequeno de encaminhamento disparando recording-state maybe-start/maybe-stop funcionaria igual.

Checklist

Portar session recording do Claude Code pra outro projeto — os 5 movimentos:

  1. cp dois scripts Python pro bin/ do projeto alvo. Sem modificação — scripts honram $CLAUDE_PROJECT_DIR, portáveis entre projetos por design.
  2. Criar .claude/settings.local.json, só hooks. Não porte a lista de permissions — permissions são estado de projeto (diferentes por projeto); hooks são padrões (idênticos entre projetos).
  3. Criar .git/hooks/post-commit (3 linhas), export CLAUDE_PROJECT_DIR=$ROOT na mão e chamar recording-state commit. Esse é o único ponto onde o mundo do git e o mundo do Claude Code são conectados pela mesma env var.
  4. Adicionar docs/notes/ ao .gitignore. Notas são transitórias + privadas, não parte do repo.
  5. Smoke test manual: CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, verifica se a inferência branch→feature tá certa. Se tree limpo, o script é projetado pra pular — não é bug.

A decisão de design real não é "como eu porto" — portar é quase cp. É dividir a lógica de gravação em 4 camadas distintas, cada uma fazendo uma coisa clara:

  • Scripts Python: helper sem estado, honra CLAUDE_PROJECT_DIR
  • Hook Claude Code: eventos internos da ferramenta (carne de session)
  • Hook git: eventos agnósticos de ferramenta (esqueleto de commit)
  • gitignore: controle de ruído

Cada uma das 4 pode ser portada, substituída ou pulada independentemente (quer suporte pra Amp? adiciona uma camada de hook Amp. Mudar formato de nota? edita o Python. Não quer git trackear notas? remove o post-commit). Claude pode escrever o código certo — mas o julgamento sobre "a qual camada essa funcionalidade pertence" não dá pra ele fazer por você. Esse é um julgamento sobre suas fronteiras de tooling, e é seu.