ส่วนที่ยากสุดของบทความเคสจริงคือการเก็บวัตถุดิบ: 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 นี้ บทความนี้ก็เช่นกัน
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 อ่านและเขียนไฟล์นี้ ไม่มีการประสานงานอื่น
"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 คือสัญญาณจริงของ "กำลังทำอะไรจริงจัง" อ่านไฟล์ รันเทส ถามคำถาม — ไม่นับ
{
"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 ของ 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 มันกำลังเขียนวัตถุดิบสรุปให้บทความในอนาคตของคุณ — คุณแค่ต้องให้มันเขียนดีตั้งแต่ครั้งแรก ไม่ต้องเขียนใหม่ทีหลัง
"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 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 ง่าย แต่ลืมแล้วร้อยบรรทัดซ้ำโผล่ทันที
/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
ตัวอย่างจริง (จาก 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 บันทึกตัวเอง