Free

Deixando o Claude escrever hooks que gravam ele mesmo

O mais difícil de artigos de caso real é juntar material: git log só tem commit messages, detalhes da sessão somem. Quatro hooks e dois slash commands transformam cada sessão em raw.md — custo marginal zero.


A parte mais difícil de escrever um artigo "Claude fez X pra mim" não é escrever — é juntar o material. O git log só tem mensagens de commit; as propostas que você descartou, as tentativas que falharam, as três revisões daquele prompt, a saída de erro — é isso que faz um artigo valer a pena, e tudo se perdeu.

Pior: na hora de escrever, você já não lembra por que escolheu A em vez de B. "Acho que tinha um motivo" versus "esse motivo era X" é a diferença entre um artigo confiável e enchimento.

Tomar ata custa mais que escrever o artigo, então você não toma. A única solução é deixar a gravação acontecer sozinha — hooks.

Fiz o Claude escrever 4 hooks + 2 slash commands que transformam toda sessão de dev em docs/notes/<feature>/raw.md. O artigo anterior, Deixando o Claude fazer deploy em produção, tirou a maioria dos commits, snippets de bash e referências de erro deste pipeline. Este também.


Visão geral do pipeline

4 hooks + 2 slash commands manuais formam uma única esteira:

Gatilho O que faz
PostToolUse(Edit\ Write\
PostToolUse(Bash) Para quando voltar a master + limpo
git post-commit Escreve um checkpoint por commit
Stop Parseia o transcript ao fim da sessão
/record-feature NAME /stop-recording Override manual

Um único arquivo de estado: docs/notes/.state.json. Todo hook lê e escreve nele. Sem outra coordenação.

Hook #1: quando começar

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

Núcleo do maybe-start:

def cmd_maybe_start():
    if load_state(): return          # já está gravando
    if not tree_is_dirty(): return   # nada mudou, pula
    feature = branch_to_feature(current_branch())
    if not feature:
        feature = "session-" + datetime.now().strftime("%Y%m%d-%H%M")
    save_state({"feature": feature, "auto_started": True, ...})

branch_to_feature("feature/pro") retorna "pro". Devs já dividem o trabalho por branch, então reuso essa fronteira mental — a razão raiz do pipeline sobreviver é que ele não exige que o usuário lembre nada novo. Em master, scripts pontuais caem para um timestamp.

Por que o matcher é Edit/Write/MultiEdit: escrever código é o sinal real de "estou de fato fazendo algo". Ler arquivos, rodar testes, fazer perguntas — não conta.

Hook #2: quando parar

{
  "matcher": "Bash",
  "hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop" }]
}
def cmd_maybe_stop():
    state = load_state()
    if not state or not state.get("auto_started"): return
    if current_branch() not in ("master", "main"): return
    if tree_is_dirty(): return
    clear_state()

Três portões: gravações manuais não param, feature branches não param, árvore suja não para.

Por que pendurar no Bash em vez de Edit: depois de mergear pra master normalmente você não edita mais, mas com certeza vai rodar git status / git log / bin/rails test — qualquer comando dá ao hook a chance de notar "hora de fechar".

A flag auto_started é o ponto de apoio. Se você dispara /record-feature pro-launch e atravessa múltiplas branches em múltiplos merges, regras de auto-stop matariam a gravação no meio. Gravações manuais nunca são auto-paradas — só /stop-recording.

Hook #3: commit como checkpoint

Não é hook do Claude Code — é .git/hooks/post-commit:

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

recording-state commit anexa a mensagem completa do commit ao raw.md:

### Commit 2026-04-16 21:55: `71f38a1`
> Add pricing page and expand account UI (P6 phases 1-2)
> 
> Pricing (/pricing):
> - Displays all 6 plans in monthly/yearly grid with Stimulus toggle
> - Anonymous users see Subscribe → sign-in flow
> ...

Por que mensagens de commit importam: são o resumo escrito à mão que você (ou o Claude) produziu no momento exato em que a feature acabou de ficar pronta e o contexto estava completo. Mais preciso que lembrar depois, mais curto que o transcript, mais abstrato que o diff.

Quando o Claude escreve um commit message, está essencialmente escrevendo material de resumo para seu artigo futuro — você só precisa que ele faça bem da primeira vez, sem reescrever depois.

Hook #4: extrair eventos do transcript

"Stop": [{
  "hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }]
}]

Quando uma sessão acaba, o Claude Code passa o transcript_path ao hook via stdin como JSON. extract-session-notes abre esse jsonl, percorre linha a linha e categoriza:

keep_patterns = (
    "test", "spec", "rspec", "minitest",
    "kamal", "git commit", "git push",
    "rails db", "rails routes", "rails runner",
    "migrate", "curl -X", "curl -s -X",
)
if any(kw in cmd for kw in keep_patterns):
    bash_cmds.append({"cmd": cmd[:400], "desc": desc})

Só comandos bash da whitelist são capturados. 90% do bash de uma sessão de código é ls / cat / grep / head — artigos não usam nada disso, tudo filtrado. O que sobra (rodar testes, kamal, rails runner, curl em APIs) são comandos com história por trás.

Prompts de usuário envolvidos em <command-*> ou <system-*> são descartados — só a entrada real sobrevive. Caminhos de Edit/Write são deduplicados num set. Saída de erro cortada em 400 caracteres. Chamadas a sub-agente Task ficam (prompt cortado em 2000 caracteres, suficiente para ver o que o sub-agente fez).

Uma gravação, vários Stops: use um cursor

O hook Stop não dispara uma vez por gravação — ele dispara toda vez que uma sessão termina. Na mesma feature branch você pode abrir e fechar o Claude Code cinco ou seis vezes. Se cada Stop re-parseasse o transcript inteiro e escrevesse tudo, o raw.md afundaria em duplicatas.

Solução: meter um cursor last_extracted_at no state file:

filter_after = state.get("last_extracted_at") or state.get("started_at")
events = extract_events(transcript_path, filter_after)
# ...depois de escrever...
state["last_extracted_at"] = datetime.now().astimezone().isoformat()
save_state(project_dir, state)

Cada passada só pega eventos depois do cursor. Simples, mas se esquecer aparecem centenas de linhas duplicadas.

Override manual: dois slash commands

/record-feature NAME:

{
  "feature": "NAME",
  "started_at": "ISO timestamp",
  "branch": "current branch",
  "auto_started": false
}

Essa linha auto_started: false desativa o auto-stop. Casos: escrever um script pontual em master mas querer deixar rastro, uma feature que cruza várias branches, ou declarar explicitamente "esse vai virar artigo".

/stop-recording: roda uma extração final no jsonl mais recente, depois limpa o state file.

Como fica o raw.md

Trecho real (de docs/notes/pro/raw.md):

## Session 2026-04-16 21:52 (`7a81bf9d`)

### User prompts
- 根据环境变量直接写好对应只,不用传。

### Files edited/written
- `config/deploy.yml`
- `config/initializers/x402.rb`

### Commit 2026-04-17 00:13: `f87ea8e`
> Add production credentials (Stripe live + x402 mainnet)
> ...

### Commit 2026-04-17 00:57: `eba9ac9`
> Fix production x402 wallet_address (stray fullwidth '?' at end)

Pra escrever um artigo: só grep. "Qual commit foi o fix do wallet address?" → eba9ac9. "Como prompteei o sub-agente do refactor?" → debaixo de ### Sub-agent invocations.

docs/notes/ está no .gitignore — é material de rascunho pra sua escrita, não código-fonte.

Três decisões de design não óbvias

1. Branch como fronteira de feature, não um sistema de metadata separado. Devs já particionam trabalho por branch; adicionar outra camada "nome de feature" inevitavelmente diverge. Reuso da fronteira existente, carga cognitiva zero.

2. Whitelist no bash, não blacklist. 95% do bash em transcripts é ruído. Manter uma lista de "o que vale guardar" fica estável por anos; manter uma de "o que filtrar" não.

3. O cursor vive no state file, não no transcript. O transcript pertence ao Claude Code; o estado pertence ao pipeline. Desacoplados — Claude Code pode mudar formato do transcript sem me quebrar, e eu posso reescrever a lógica de extração sem tocar transcript.

Fechando o ciclo

Escrever um artigo de 1500 palavras consome material de uns 30 commits, 4 sessões, uma dúzia de comandos bash chave. Juntar à mão custa cerca de uma hora — suficiente pra você cortar caminho na próxima.

Que os hooks façam: custo marginal zero. Instale isso uma vez e simplesmente trabalhe normalmente — saque uma branch, escreva código, deploy — e o material do artigo se acumula sozinho.

Cada referência de commit, snippet de bash, linha de erro e caminho de arquivo neste artigo veio de docs/notes/pro/raw.md — material de rascunho que o hook gravou sozinho desde o momento em que saquei feature/pro há dois dias. Quando chegou a hora de escrever, abri o arquivo e tudo que precisava estava lá.

A melhor forma de fazer o Claude escrever artigos sobre Claude Code é primeiro fazer o Claude escrever hooks pra gravar a si mesmo.