Hệ ghi của how2claude sang dự án Rails khác: 4 file, 5 bước, lớp hook.
how2claude có một bộ hooks tự động ghi session Claude Code — bắt đầu ghi khi công việc khởi động, gắn checkpoint mỗi commit, và khi session kết thúc, trích prompt / bash / danh sách edit vào docs/notes/<feature>/raw.md. Bài let-claude-record-itself đã kể cách dựng.
Vấn đề: dự án còn lại của tôi (smarts, site tài liệu cho smart contract) chẳng có gì. Mỗi lần muốn viết bài sau đó, lại phải bới git log cộng trí nhớ, thấy luôn hụt phần hay. Bài này kể chuyện chuyển hệ ghi sang đó — tổng cộng 4 file, 5 phút — nhưng dọc đường có một nhận xét thật về lớp hook: hooks của Claude Code và hooks của git hoạt động ở hai lớp hoàn toàn khác nhau, và cái gì được bắt khi bạn trộn công cụ (ví dụ Amp + Claude Code) phụ thuộc vào lớp bạn cài vào.
Tất cả trong how2claude liên quan đến ghi session:
| File | Lớp | Vai trò |
|---|---|---|
bin/recording-state |
script | helper Python, quản lý vòng đời .state.json |
bin/extract-session-notes |
script | helper Python, đọc transcript Claude Code → ghi raw.md |
.claude/settings.local.json |
hook Claude Code | PostToolUse / Stop kích các script trên |
.git/hooks/post-commit |
hook git | mỗi commit gọi recording-state commit để checkpoint |
.gitignore |
kiểm soát nhiễu | giữ docs/notes/ ngoài repo (ghi chú là riêng tư/tạm thời) |
4 mảnh, 4 lớp khác nhau. Sự phân biệt này quay lại dưới đây.
Hai hệ hook chạy cùng lúc, phạm vi hoàn toàn khác:
Hooks Claude Code (định nghĩa trong .claude/settings.local.json):
- Phạm vi: chỉ bắn bên trong công cụ Claude Code
- Kích: PostToolUse / Stop / PreToolUse — sự kiện vòng đời Claude Code
- Thông tin có: tên tool, args, transcript_path (jsonl session đầy đủ) — thứ chỉ Claude Code biết
Hooks git (script shell dưới .git/hooks/):
- Phạm vi: bắn ở mọi sự kiện git, không quan trọng ai kích git
- Kích: post-commit / pre-push / v.v.
- Thông tin có: cái git tự biết (sha, tác giả, branch, diff)
Hệ quả thực tế: viết code + commit bên trong Claude Code thì cả hai lớp hook cùng bắn — thông tin session và thông tin commit đều rơi vào raw.md. Chuyển sang Amp (hay Cursor, hay gõ tay) để viết + commit thì chỉ hook git bắn — raw.md có xương commit nhưng không có prompt / bash / chi tiết edit của session.
Đây không phải lỗi — đây là ràng buộc thiết kế của mỗi công cụ. Muốn chi tiết mức session dưới mọi công cụ có nghĩa cài một lớp hook riêng cho mỗi công cụ. Dự phòng git cho bạn "đã làm gì"; không cho bạn "đã nghĩ gì, vỡ ở đâu".
Hướng dẫn chọn:
- Xương độc lập công cụ (thông tin commit, thay đổi code) → để ở hook git
- Thịt đặc thù Claude Code (prompt đầy đủ, mạch suy nghĩ) → để ở hook Claude Code
- Cả hai → cài ở cả hai lớp
Cài cùng bộ ghi vào smarts (/home/bob/Work/smarts, dự án Rails).
mkdir -p /home/bob/Work/smarts/bin
cp /home/bob/Work/how2claude/bin/recording-state \
/home/bob/Work/smarts/bin/recording-state
cp /home/bob/Work/how2claude/bin/extract-session-notes \
/home/bob/Work/smarts/bin/extract-session-notes
chmod +x /home/bob/Work/smarts/bin/{recording-state,extract-session-notes}
Script không đổi — dùng env var $CLAUDE_PROJECT_DIR để quyết định ghi vào đâu:
def project_dir():
return os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
def state_path():
return pathlib.Path(project_dir()) / "docs/notes/.state.json"
Đây là trừu tượng chốt: Claude Code tự set CLAUDE_PROJECT_DIR khi bắn hook; ta set thủ công bên trong post-commit của git. Cả hai phía tôn trọng cùng một env var, và script không cần tự hỏi "tôi đang ở dự án nào".
.claude/settings.local.jsonMột quyết định tại đây: chỉ chuyển hooks, không chuyển danh sách permissions.
settings.local.json của how2claude có hơn 100 entry permissions.allow — tất cả đặc thù how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Không có ý nghĩa gì khi mang sang smarts. smarts sẽ tích lũy permissions riêng một cách tự nhiên khi dùng.
Hooks là khuôn mẫu — giống nhau giữa các dự án. Permissions là trạng thái dự án — khác nhau giữa các dự án.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop"
}
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }
]
}
]
}
}
Ba điểm kích:
- File được sửa → thử bắt đầu ghi (maybe-start tự kiểm: nếu đang ghi thì bỏ qua, nếu tree clean cũng bỏ qua)
- Lệnh bash chạy → thử dừng (maybe-stop chặt: chỉ dừng khi "tự khởi động VÀ quay lại master VÀ tree clean" đồng thời đúng)
- Session kết thúc → trích transcript vào raw.md
.git/hooks/post-commit3 dòng:
#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true
Export thủ công CLAUDE_PROJECT_DIR=$ROOT là điểm chính xác mà thế giới git và thế giới Claude Code được nối qua cùng một env var. || true đảm bảo hook không bao giờ chặn commit.
chmod +x /home/bob/Work/smarts/.git/hooks/post-commit
docs/notes/ vào .gitignore# Session recording notes (transient, for article material)
docs/notes/
Ghi chú là tạm + riêng tư — bạn không muốn raw.md được commit vào PR; không muốn .state.json làm bẩn git status. gitignore của how2claude cũng xử vậy.
Kích maybe-start thủ công ngay sau khi cài:
$ cd /home/bob/Work/smarts && CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start
[recording] auto-started: contract-to-docs (branch: feat/contract-to-docs)
$ cat docs/notes/.state.json
{
"feature": "contract-to-docs",
"started_at": "2026-04-20T17:47:18-04:00",
"branch": "feat/contract-to-docs",
"auto_started": true
}
Script nhận đúng rằng smarts đang tình cờ ở branch feat/contract-to-docs với tree dirty — tự khởi động ghi, suy tên feature contract-to-docs từ branch feat/contract-to-docs. Logic trong script:
def branch_to_feature(branch):
if not branch or branch in ("master", "main"):
return None
if "/" in branch:
return branch.split("/", 1)[1]
return branch
feat/X → X, feature/X → X, fix-y trần → fix-y, master/main → None (rơi về tên có timestamp session-YYYYMMDD-HHMM).
Heuristic này ngốc nghếch đủ vừa — tên branch chính là chủ đề công việc, không cần bắt bạn đặt tên lại.
Sau khi cài tôi tự hỏi: thỉnh thoảng tôi làm việc trong Amp — phần đó có được bắt không? Câu trả lời đúng như câu chuyện về lớp hook dự đoán:
| Kịch bản | Hook Claude Code | Hook git | Cái gì rơi vào ghi chú |
|---|---|---|---|
| Làm trong Claude Code + commit | ✅ bắn | ✅ bắn | chi tiết session + xương commit |
| Làm trong Amp + commit | ❌ không bắn | ✅ bắn | chỉ xương commit |
| Gõ tay + commit | ❌ không bắn | ✅ bắn | chỉ xương commit |
| Làm trong Claude Code, chưa commit | ✅ khởi động ghi | — | chi tiết session (mục commit đợi commit kế) |
Kết luận: đủ dùng nếu Claude Code là công cụ chính. Xương commit luôn có; chi tiết session chỉ có trên đường Claude Code. Cho bài "đã làm gì" bạn chủ yếu dựa vào body commit — luồng prompt / bash của session là điểm cộng, có thì tốt nhưng không bắt buộc.
Nếu bạn dùng Amp nhiều, Amp có hệ hook riêng (tôi chưa đào sâu); một script chuyển tiếp nhỏ kích recording-state maybe-start/maybe-stop sẽ làm giống hệt.
Chuyển session recording của Claude Code sang dự án khác — 5 bước:
cp hai script Python vào bin/ của dự án đích. Không đổi — script tôn trọng $CLAUDE_PROJECT_DIR, khả chuyển giữa dự án theo thiết kế..claude/settings.local.json, chỉ hooks. Đừng chuyển danh sách permissions — permissions là trạng thái dự án (khác theo dự án); hooks là khuôn mẫu (giống nhau giữa dự án)..git/hooks/post-commit (3 dòng), export CLAUDE_PROJECT_DIR=$ROOT thủ công và gọi recording-state commit. Đó là điểm duy nhất nơi thế giới git và thế giới Claude Code được nối qua cùng một env var.docs/notes/ vào .gitignore. Ghi chú là tạm + riêng tư, không thuộc repo.CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, xác nhận suy luận branch→feature đúng. Nếu tree clean, script được thiết kế để bỏ qua — không phải lỗi.Quyết định thiết kế thực sự không phải "chuyển thế nào" — chuyển gần như chỉ là cp. Nó là tách logic ghi thành 4 lớp khác biệt, mỗi lớp làm đúng một việc rõ ràng:
CLAUDE_PROJECT_DIRMỗi trong 4 lớp có thể được chuyển, thay, hoặc bỏ độc lập (muốn hỗ trợ Amp? thêm lớp hook Amp. Đổi định dạng ghi chú? sửa Python. Không muốn git theo dõi ghi chú? xóa post-commit). Claude có thể viết code đúng — nhưng phán đoán "chức năng này thuộc lớp nào" thì nó không thể làm thay bạn. Đó là phán đoán về ranh giới công cụ của bạn, và nó là của bạn.