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.
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.
"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.
{
"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.
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.
"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).
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.
/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.
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.
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.
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.