Free

Membiarkan Claude Menulis Hook yang Merekam Dirinya Sendiri

Yang tersulit dari artikel kasus nyata adalah kumpulin materi: git log cuma punya commit message, detail sesi hilang. 4 hook + 2 slash command ubah tiap sesi jadi raw.md — biaya marjinal nol.


Bagian tersulit dari menulis artikel "Claude bantu saya melakukan X" bukan menulisnya — tapi mengumpulkan materinya. git log cuma ada commit message; usulan yang kamu tolak, percobaan yang gagal, prompt yang kamu revisi tiga kali, output error — itu yang bikin artikel layak dibaca, dan semuanya hilang.

Lebih buruk: saat kamu menulis artikel, kamu sudah lupa kenapa pilih A bukannya B. "Saya ingat ada alasannya" vs "alasannya adalah X" itu beda level kredibilitas.

Bikin notulen lebih mahal daripada nulis artikelnya, jadi kamu nggak nulis. Satu-satunya solusi adalah biarin perekaman terjadi sendiri — hook.

Saya suruh Claude nulis 4 hook + 2 slash command yang ngubah tiap sesi dev jadi docs/notes/<feature>/raw.md. Artikel sebelumnya, Membiarkan Claude Deploy ke Production, ngambil mayoritas commit, bash snippet, dan referensi error dari pipeline ini. Artikel ini juga.


Gambaran pipeline

4 hook + 2 slash command manual jadi satu jalur:

Pemicu Apa yang dia lakukan
PostToolUse(Edit\ Write\
PostToolUse(Bash) Stop kalau balik ke master + bersih
git post-commit Tulis checkpoint per commit
Stop Parse transcript saat sesi berakhir
/record-feature NAME /stop-recording Override manual

Satu file state: docs/notes/.state.json. Semua hook baca dan tulis ini. Tanpa koordinasi lain.

Hook #1: kapan mulai

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

Inti maybe-start:

def cmd_maybe_start():
    if load_state(): return          # sudah merekam
    if not tree_is_dirty(): return   # nggak ada perubahan, skip
    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") mengembalikan "pro". Developer sudah biasa pisah kerjaan per branch, jadi kita pakai ulang batas mental itu — alasan dasar pipeline ini bertahan: nggak minta user inget hal baru. Di master, script sekali pakai jatuh ke timestamp.

Kenapa matcher-nya Edit/Write/MultiEdit: nulis kode itu sinyal nyata "saya benar-benar lagi ngerjain sesuatu". Baca file, jalanin test, tanya — nggak ngitung.

Hook #2: kapan berhenti

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

Tiga gerbang: rekaman manual nggak berhenti, di feature branch nggak berhenti, tree kotor nggak berhenti.

Kenapa nyantol di Bash bukannya Edit: setelah merge ke master biasanya kamu nggak edit lagi, tapi pasti jalanin git status / git log / bin/rails test — perintah apapun ngasih hook kesempatan ngecek "saatnya selesai".

Flag auto_started itu kunci. Kalau kamu mulai /record-feature pro-launch dan lewat banyak branch banyak merge, aturan auto-stop bakal salah matiin di tengah-tengah feature. Rekaman manual nggak pernah auto-stop — cuma /stop-recording.

Hook #3: commit sebagai checkpoint

Bukan hook Claude Code — ini .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 nempelin commit message lengkap ke 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
> ...

Kenapa commit message penting: itu ringkasan tulis-tangan yang kamu (atau Claude) bikin pas feature baru selesai dan konteks masih utuh. Lebih akurat daripada inget belakangan, lebih pendek daripada transcript, lebih abstrak daripada diff.

Pas Claude nulis commit message, dia esensinya nulis material ringkasan untuk artikel masa depanmu — kamu cuma butuh dia nulis bagus dari awal, nggak perlu nulis ulang nanti.

Hook #4: ekstrak event dari transcript

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

Pas sesi berakhir, Claude Code passing transcript_path ke hook lewat stdin JSON. extract-session-notes buka jsonl itu, parse baris demi baris, dan kategorikan:

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

Cuma command bash di whitelist yang dicapture. 90% bash di sesi koding adalah ls / cat / grep / head — artikel nggak butuh, semua dibuang. Sisanya (jalanin test, kamal, rails runner, curl ke API) baru command yang ada ceritanya.

User prompt yang dibungkus <command-*> atau <system-*> dibuang — cuma input asli yang lewat. Path Edit/Write didedup ke set. Output error dipotong 400 karakter. Pemanggilan sub-agent Task disimpan (prompt dipotong 2000 karakter, cukup buat lihat sub-agent ngapain).

Satu rekaman, banyak Stop: pakai cursor

Hook Stop nggak nge-fire sekali per rekaman — dia nge-fire tiap sesi berakhir. Di feature branch yang sama kamu mungkin buka-tutup Claude Code lima enam kali. Kalau tiap Stop re-parse transcript penuh dan tulis semua, raw.md bakal tenggelam dalam duplikat.

Solusi: taruh cursor last_extracted_at di state file:

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

Tiap pass cuma ambil event setelah cursor. Sederhana, tapi lupa = ratusan baris duplikat.

Override manual: dua slash command

/record-feature NAME:

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

Baris auto_started: false itu nonaktifin auto-stop. Kasus pakai: nulis script sekali pakai di master tapi pengen ada jejak, feature yang lewat banyak branch, atau eksplisit nyatain "yang ini buat artikel".

/stop-recording: jalanin ekstraksi final terhadap jsonl terbaru, terus bersihin state file.

raw.md kelihatannya gimana

Cuplikan asli (dari 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)

Buat nulis artikel: tinggal grep. "Commit yang fix wallet address tuh yang mana?" → eba9ac9. "Gimana saya prompt sub-agent buat refactor itu?" → di bawah ### Sub-agent invocations.

docs/notes/ ada di .gitignore — itu material draft buat tulisanmu, bukan source code.

Tiga keputusan desain yang nggak obvious

1. Branch sebagai batas feature, bukan sistem metadata terpisah. Developer sudah misah kerjaan per branch; nambah layer "nama feature" pasti drift. Pakai ulang batas yang ada, beban kognitif nol.

2. Whitelist bash, bukan blacklist. 95% bash di transcript itu noise. Maintain list "yang layak disimpan" stabil bertahun-tahun; maintain list "yang harus difilter" enggak.

3. Cursor tinggal di state file, bukan di transcript. Transcript milik Claude Code; state milik pipeline. Decoupled — Claude Code bisa ganti format transcript tanpa rusakin saya, dan saya bisa rewrite logic ekstraksi tanpa nyentuh transcript.

Menutup loop

Nulis artikel 1500 kata makan material sekitar 30 commit, 4 sesi, belasan command bash kunci. Ngumpulin manual sekitar sejam — cukup bikin kamu cari jalan pintas di artikel berikutnya.

Biarin hook yang ngerjain: biaya marjinal nol. Pasang sekali terus kerja seperti biasa — pull branch, tulis kode, ship — dan material artikel numpuk sendiri.

Tiap referensi commit, snippet bash, baris error, dan path file di artikel ini berasal dari docs/notes/pro/raw.md — material draft yang hook itu rekam sendiri sejak saya checkout feature/pro dua hari lalu. Pas waktunya nulis, saya buka file dan semua yang dibutuhkan ada di sana.

Cara terbaik bikin Claude nulis artikel tentang Claude Code adalah duluan suruh Claude nulis hook buat ngerekam dirinya sendiri.