免費

讓 Claude 寫 hook 記錄自己的 session

寫實戰文章最難的是湊素材:git log 只有 commit message,session 裡的關鍵細節全丟。4 個 hook + 2 個 slash command 把 session 自動變成 raw.md,素材 0 邊際成本堆出來。


寫一篇「Claude 幫我做了 X」的實戰文章,最難的不是寫,是湊素材。回頭扒 git log 只有 commit message,session 裡被你否決的方案、失敗的嘗試、改過三次的提示詞、報錯的輸出——這些才是文章最值錢的部分,全丟了。

更糟的是:你寫文章時已經記不清當時為什麼選 A 不選 B。「我記得有個理由」和「那個理由是 X」差著一篇文章的可信度。

寫會議記錄的成本遠高於寫文章,所以你不會記。能解決這件事的,只有讓記錄這件事自己發生——hook。

讓 Claude 寫了 4 個 hook + 2 個 slash command,把每次開發 session 自動變成 docs/notes/<feature>/raw.md。前一篇《讓 Claude 做生產部署》引用的大量 commit、bash 命令、錯誤片段,全部從這個 pipeline 出來。本文也是。


Pipeline 全景

4 個 hook + 2 個手動 slash command 串起一條流水線:

觸發器 幹啥
PostToolUse(Edit\ Write\
PostToolUse(Bash) 回 master + 乾淨就停錄
git post-commit 每個 commit 寫一筆 checkpoint
Stop session 結束時解析 transcript 抽事件
/record-feature NAME /stop-recording 手動覆蓋

狀態檔只有一個:docs/notes/.state.json。所有 hook 都讀它寫它,沒有別的協調。

Hook #1:何時開始錄

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

maybe-start 核心邏輯:

def cmd_maybe_start():
    if load_state(): return          # 已經在錄
    if not tree_is_dirty(): return   # 沒動東西不錄
    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") 返回 "pro"。開發者已經習慣切 branch 寫功能了,直接複用這個心智邊界——不讓使用者多記任何東西,是這套 pipeline 能持續運行的根本原因。在 master 上寫一次性腳本就 fallback 到時間戳。

掛在 Edit/Write 上的原因:寫程式這件事是 session 真正「開幹」的信號。光問問題、看檔案、跑測試都不算。

Hook #2:何時停錄

{
  "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()

三道閘:手動啟動的不停,feature branch 上不停,樹髒不停。

掛在 Bash 上而不是 Edit 上的原因:合併回 master 之後通常不會再 Edit,但一定會 git status / git log / bin/rails test——只要敲一下命令,hook 就有機會判斷「該收尾了」。

auto_started 這個標記是關鍵。如果你 /record-feature pro-launch 手動開了一段記錄,跨多個 branch 跨多次合併,自動停止規則會誤殺。手動開的從不自動停,只能 /stop-recording

Hook #3:commit 即 checkpoint

不是 Claude Code hook,是 .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 把 commit message 完整寫進 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
> ...

為什麼 commit message 這麼重要:那是你(或 Claude)在做完功能、有完整上下文時手寫的摘要。比事後回憶準,比 transcript 短,比 diff 抽象。

Claude 寫 commit message 的時候,相當於在幫你寫文章的素材摘要——你只要讓它寫好,不用回頭再寫一遍。

Hook #4:從 transcript 抽事件

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

session 結束時,Claude Code 會把 transcript_path 通過 stdin JSON 傳給 hook。extract-session-notes 打開這個 jsonl,按行解析,分類提取:

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})

只抽白名單裡的 bash 命令。一個寫程式的 session 裡 90% 的 bash 是 ls / cat / grep / head——文章用不到,全過濾掉。剩下的(跑測試、跑 kamal、跑 runner、curl 接口)才是有故事的命令。

使用者 prompt 裡 <command-*><system-*> 包裹的全部丟掉,只留真實輸入。Edit/Write 的路徑去重存 set。報錯只留前 400 字。Task 子 agent 呼叫保留(prompt 截到 2000 字元,看清子 agent 幹了啥足夠)。

一個 session 多次 Stop:用 cursor 去重

Stop hook 不是一個 recording 週期觸發一次,是每次 session 結束都觸發。同一個 feature branch 上你可能開關 Claude Code 五六次,如果每次都 re-parse 整個 transcript 全量寫入,raw.md 會被同樣的事件灌爆。

解決:state file 裡塞一個游標 last_extracted_at

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

每次只抽游標之後的事件。簡單,但漏了就重複,一抽就是幾百行。

手動覆蓋:兩個 slash command

/record-feature NAME

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

auto_started: false 這一行讓自動停止失效。適合:在 master 上寫一次性腳本但還想留記錄、跨多個 branch 的大功能、明確「這是要寫文章的」。

/stop-recording:手動跑一次 final extraction(最近一個 jsonl),清掉 state file。

raw.md 長什麼樣

真實片段(截自 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)

寫文章時直接 grep:「要找錢包位址那個 bug 是哪個 commit?」→ eba9ac9。「那次重構子 agent 的 prompt 是怎麼寫的?」→ 在 ### Sub-agent invocations 裡。

docs/notes/.gitignore 裡——是給你寫文章用的草稿料,不是原始碼。

三個非顯然的設計決策

1. branch 作為 feature 邊界,而不是另搞一套元資料。 使用者已經在用 branch 切分工作,多套一層「功能名」必然 drift。複用現有邊界,0 額外認知負擔。

2. bash 白名單而不是黑名單。 transcript 裡 95% 的 bash 是雜訊。維護「哪些值得記」的列表,比維護「哪些要過濾掉」的列表,長期下來穩定 10 倍。

3. cursor 在 state file 裡,不在 transcript 裡。 transcript 是 Claude Code 擁有的,state 是 pipeline 擁有的。兩邊解耦,Claude Code 改 transcript 格式我不影響,pipeline 改邏輯也不需要碰 transcript。

閉環

寫完一篇 1500 字的文章大約要 30 個 commit、4 次 session、十幾條關鍵 bash 命令的素材。手動整理一次大概 1 小時,足夠讓你下次直接編。

讓 hook 自動記,0 邊際成本。這套 pipeline 裝好之後,你只要正常切 branch 寫程式,文章素材自己就堆出來了。

本文引用的 commit、bash 命令、錯誤片段、檔案路徑,全部來自 docs/notes/pro/raw.md——這份草稿料,是從兩天前切 feature/pro 的那一刻起 hook 自己記下來的。等到要寫文章才打開看,發現該有的都有。

讓 Claude 寫文章關於 Claude Code,最好的方式是先讓 Claude 寫 hook 記錄自己。