Free

ให้ Claude เขียน hook บันทึก session ของตัวเอง

ส่วนที่ยากสุดของบทความเคสจริงคือการเก็บวัตถุดิบ: git log มีแค่ commit message รายละเอียด session หายหมด. 4 hook + 2 slash command แปลง session เป็น raw.md — ต้นทุนส่วนเพิ่มเป็นศูนย์.


ส่วนที่ยากที่สุดของการเขียนบทความ "Claude ช่วยผมทำ X" ไม่ใช่การเขียน — คือการเก็บวัตถุดิบ git log มีแค่ commit message เท่านั้น ข้อเสนอที่คุณปฏิเสธ ความพยายามที่ล้มเหลว prompt ที่แก้สามรอบ output error — นี่คือส่วนที่ทำให้บทความน่าอ่าน และมันหายหมดแล้ว

แย่กว่านั้น ตอนคุณนั่งเขียนจริง ๆ คุณจำไม่ได้แล้วว่าทำไมเลือก A ไม่ใช่ B "จำได้ว่ามีเหตุผล" กับ "เหตุผลนั้นคือ X" คือความต่างระหว่างบทความน่าเชื่อถือกับบทความยัดไส้

การจดบันทึกประชุมแพงกว่าการเขียนบทความเอง คุณจึงไม่จด ทางออกเดียวคือปล่อยให้การบันทึกเกิดขึ้นเอง — hook

ผมให้ Claude เขียน 4 hook + 2 slash command ที่แปลง session การพัฒนาแต่ละครั้งเป็น docs/notes/<feature>/raw.md บทความก่อนหน้า ให้ Claude deploy ขึ้น production ดึง commit, snippet bash และ reference error ส่วนใหญ่มาจาก pipeline นี้ บทความนี้ก็เช่นกัน


ภาพรวม pipeline

4 hook + 2 slash command แบบ manual รวมเป็นสายพานเดียว:

ตัวกระตุ้น ทำอะไร
PostToolUse(Edit\ Write\
PostToolUse(Bash) หยุดเมื่อกลับ master + สะอาด
git post-commit เขียน checkpoint ต่อ commit
Stop parse transcript ตอนจบ session
/record-feature NAME /stop-recording override manual

ไฟล์ state ไฟล์เดียว: 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 เป็น timestamp

ทำไม matcher ถึงเป็น Edit/Write/MultiEdit: การเขียน code คือสัญญาณจริงของ "กำลังทำอะไรจริงจัง" อ่านไฟล์ รันเทส ถามคำถาม — ไม่นับ

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

สามประตู: การบันทึก manual ไม่หยุด feature branch ไม่หยุด tree สกปรกไม่หยุด

ทำไมผูกกับ Bash ไม่ใช่ Edit: หลัง merge กลับ master ปกติจะไม่ edit อีก แต่แน่นอนต้องรัน git status / git log / bin/rails test — คำสั่งอะไรก็ได้ให้โอกาส hook ตระหนักว่า "เวลาปิดแล้ว"

flag auto_started คือจุดรับน้ำหนัก ถ้าคุณสั่ง /record-feature pro-launch แบบ manual แล้วผ่านหลาย branch หลาย merge กฎ auto-stop จะฆ่าการบันทึกกลาง feature การบันทึก manual ไม่ auto-stop เด็ดขาด — ต้อง /stop-recording เท่านั้น

Hook #3: commit คือ checkpoint

ไม่ใช่ hook ของ 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 ต่อท้าย 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) สร้างในช่วงที่ feature เพิ่งเสร็จและ context ยังเต็ม แม่นยำกว่าคิดย้อน สั้นกว่า transcript นามธรรมกว่า diff

เมื่อ Claude เขียน commit message มันกำลังเขียนวัตถุดิบสรุปให้บทความในอนาคตของคุณ — คุณแค่ต้องให้มันเขียนดีตั้งแต่ครั้งแรก ไม่ต้องเขียนใหม่ทีหลัง

Hook #4: ดึง event จาก transcript

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

เมื่อ session จบ Claude Code ส่ง transcript_path ให้ hook ผ่าน stdin เป็น JSON extract-session-notes เปิด jsonl นั้น parse ทีละบรรทัด จัดหมวด:

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 ที่อยู่ใน whitelist 90% ของ bash ใน session เขียน code คือ ls / cat / grep / head — บทความไม่ได้ใช้ กรองทิ้งหมด เหลือ (รันเทส, kamal, rails runner, curl API) — คำสั่งที่มีเรื่องเล่า

prompt ของผู้ใช้ที่ห่อด้วย <command-*> หรือ <system-*> ทิ้งหมด เหลือเฉพาะ input จริง path Edit/Write dedup เก็บใน set output error ตัด 400 ตัวอักษร การเรียก Task sub-agent เก็บไว้ (prompt ตัด 2000 ตัวอักษร พอเห็นว่า sub-agent ทำอะไร)

การบันทึกเดียว Stop หลายครั้ง: ใช้ cursor

Stop hook ไม่ยิงครั้งเดียวต่อการบันทึก — ยิง ทุกครั้งที่ session จบ บน feature branch เดียวกันคุณอาจเปิด-ปิด Claude Code 5-6 ครั้ง ถ้า Stop แต่ละครั้ง re-parse transcript ทั้งหมดแล้วเขียนทุกอย่าง raw.md จะจมในข้อมูลซ้ำ

วิธีแก้: ใส่ cursor last_extracted_at ในไฟล์ state:

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)

แต่ละรอบเอาเฉพาะ event หลัง cursor ง่าย แต่ลืมแล้วร้อยบรรทัดซ้ำโผล่ทันที

override manual: สอง slash command

/record-feature NAME:

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

บรรทัด auto_started: false ปิด auto-stop ใช้เมื่อ: เขียนสคริปต์ใช้ครั้งเดียวบน master แต่ยังอยากทิ้งร่องรอย, feature ที่ข้ามหลาย branch, หรือประกาศชัดว่า "อันนี้สำหรับบทความ"

/stop-recording: รัน extraction สุดท้ายกับ jsonl ล่าสุด แล้วล้างไฟล์ state

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 "commit ไหนเป็น fix wallet address?" → eba9ac9 "prompt sub-agent refactor เขียนยังไง?" → ใต้ ### Sub-agent invocations

docs/notes/ อยู่ใน .gitignore — เป็นวัตถุดิบ draft สำหรับเขียน ไม่ใช่ source code

สามการตัดสินใจออกแบบที่ไม่เห็นชัด

1. branch เป็นขอบเขต feature ไม่ใช่ระบบ metadata แยก นักพัฒนาแบ่งงานตาม branch อยู่แล้ว เพิ่มเลเยอร์ "ชื่อ feature" อีกชั้นจะ drift แน่นอน ใช้ขอบเขตเดิมซ้ำ ภาระการรับรู้ = 0

2. whitelist bash ไม่ใช่ blacklist 95% ของ bash ใน transcript คือ noise การรักษารายการ "อะไรควรเก็บ" นิ่งได้หลายปี การรักษารายการ "อะไรควรกรองออก" ไม่

3. cursor อยู่ในไฟล์ state ไม่ใช่ใน transcript transcript เป็นของ Claude Code state เป็นของ pipeline decouple — Claude Code เปลี่ยน format transcript โดยไม่ทำให้ผมพัง และผมเขียน logic extraction ใหม่โดยไม่ต้องแตะ transcript

ปิดลูป

เขียนบทความ 1500 คำกินวัตถุดิบประมาณ 30 commit, 4 session, คำสั่ง bash หลัก 12 คำสั่ง เก็บเองหนึ่งชั่วโมง — พอให้ครั้งหน้าคุณตัดมุม

ให้ hook ทำ: ต้นทุนส่วนเพิ่ม = 0 ติดตั้งครั้งเดียวแล้วทำงานตามปกติ — pull branch, เขียน code, deploy — วัตถุดิบบทความกองขึ้นเอง

ทุก reference ของ commit, snippet bash, บรรทัด error, path file ในบทความนี้มาจาก docs/notes/pro/raw.md — วัตถุดิบ draft ที่ hook บันทึกเองตั้งแต่นาทีที่ผม pull feature/pro เมื่อสองวันก่อน ตอนถึงเวลาเขียน ผมเปิดไฟล์แล้วทุกอย่างที่ต้องการอยู่ครบ

วิธีที่ดีที่สุดให้ Claude เขียนบทความเรื่อง Claude Code คือเริ่มจากให้ Claude เขียน hook บันทึกตัวเอง