how2claude の録画システムを別 Rails プロジェクトへ:4 ファイル、5 手、hook 層分け。
how2claude には Claude Code session を自動録画する hooks 一式がある——作業開始時に自動で録画起動、commit 毎に checkpoint 追記、session 終了時に prompt / bash / edit リストを docs/notes/<feature>/raw.md に抽出する。let-claude-record-itself の記事でどう組み立てたかを書いた。
問題は、もう一つのプロジェクト(smarts、スマートコントラクトドキュメントサイト)にはこれが無いこと。後から記事を書こうとする度に git log と記憶を掘り返して、肝心なところが抜け落ちる感覚があった。この記事は録画システムをそっちに移植する話——結局 4 ファイル、5 分で終わった。でも途中で hook の層分けに関する本物の洞察が出てきた:Claude Code hooks と git hooks は完全に異なる層で動いていて、ツールを混在させた時(例えば Amp + Claude Code)に何が捕捉され何が漏れるかは、どの層に置いたかで決まる。
how2claude で録画に関わるもの全部:
| ファイル | 層 | 役割 |
|---|---|---|
bin/recording-state |
スクリプト | Python helper、.state.json のライフサイクル管理 |
bin/extract-session-notes |
スクリプト | Python helper、Claude Code transcript を読んで raw.md に書く |
.claude/settings.local.json |
Claude Code hook | PostToolUse / Stop が上のスクリプトを起動 |
.git/hooks/post-commit |
git hook | commit 毎に recording-state commit を呼んで checkpoint |
.gitignore |
ノイズ制御 | docs/notes/ をリポジトリから除外(ノートはプライベート/一時的) |
4 つのピース、それぞれが4 つの異なる層に座っている。この区別は以降で繰り返し使う。
2 種類の hook メカニズムが同時に走っているが、スコープが全く違う:
Claude Code hooks(.claude/settings.local.json で定義):
- スコープ:Claude Code というツールの内部でのみ発火
- トリガー:PostToolUse / Stop / PreToolUse 等、Claude Code ライフサイクルイベント
- 取れる情報:tool name、引数、transcript_path(session 完全 jsonl)——これらは Claude Code 固有
git hooks(.git/hooks/ 下の shell スクリプト):
- スコープ:git というツールが発生させた全イベント、誰が git を動かしたかに関わらず
- トリガー:post-commit / pre-push 等、各種 git イベント
- 取れる情報:git 自身が知っているもの(sha、author、branch、diff)
実際の帰結:Claude Code でコード書いて + commit すると両層の hook が発火、session 情報と commit 情報両方が raw.md に入る。Amp(または Cursor、手打ち)に切り替えて書いて + commit すると、git hook だけが発火、raw.md には commit 骨組みしか残らない、session の prompt / bash / edit 詳細は欠落。
これはバグではない——各ツールの設計制約だ。全ツール下で session レベルの詳細を取りたいなら、各ツールに自分の hook 層を仕込む必要がある。git の最終防衛線が取れるのは「何をしたか」、「何を考えたか、何で詰まったか」ではない。
選択指針:
- ツール非依存の骨組み(commit 情報、コード変更)→ git hook に置く
- Claude Code 固有の肉(完全 prompt、思考過程)→ Claude Code hook に置く
- 両方欲しい → 両層に置く
smarts(/home/bob/Work/smarts、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}
スクリプトは無修正——$CLAUDE_PROJECT_DIR 環境変数で書き込み先を決める:
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"
これが鍵となる抽象:Claude Code は hook 発火時に CLAUDE_PROJECT_DIR を自動設定、git post-commit では手動設定——両側が同じ環境変数を尊重、スクリプトは「自分がどのプロジェクトにいる」を判定する必要が無い。
.claude/settings.local.json を新規作成ここで判断が一つ:hooks だけ移植、permissions リストは移植しない。
how2claude の settings.local.json には 100 以上の permissions.allow エントリがある——全部 how2claude 特有(curl localhost:3000、bin/rails runner、kamal app exec)。これを smarts に持って行く意味はゼロ。smarts は使う中で自然に自分の permissions を蓄積する。
hooks はパターン——プロジェクト跨いで同じ。permissions はプロジェクトの状態——プロジェクト跨いで違う。
{
"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" }
]
}
]
}
}
3 つのトリガーポイント:
- ファイル編集 → 録画起動を試行(maybe-start は自己チェック:既に録画中ならスキップ、tree clean でもスキップ)
- Bash コマンド実行 → 録画停止を試行(maybe-stop は厳格:「自動起動 + master に戻っている + tree clean」3 条件同時成立でのみ停止)
- session 終了 → transcript を抽出して raw.md に書く
.git/hooks/post-commit を新規作成3 行:
#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true
手動の CLAUDE_PROJECT_DIR=$ROOT export が、git 世界と Claude Code 世界を同じ環境変数で橋渡しする正確な一点。|| true で hook が commit をブロックすることは絶対にない。
chmod +x /home/bob/Work/smarts/.git/hooks/post-commit
.gitignore に docs/notes/ 追加# Session recording notes (transient, for article material)
docs/notes/
ノートは一時的 + プライベート——raw.md を PR に commit したくない、.state.json で git status を汚したくない。how2claude の gitignore も同じ処理。
インストール直後に maybe-start を手動トリガー:
$ 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
}
スクリプトは smarts が当時ちょうど feat/contract-to-docs ブランチで tree dirty だったことを正しく認識——自動で録画起動、branch feat/contract-to-docs から feature 名 contract-to-docs を推論。そのロジック:
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 → fix-y、master/main → None(これは timestamp 付き session-YYYYMMDD-HHMM 名にフォールバック)。
このヒューリスティックはちょうど良い無脳さ——ブランチ名こそが作業の主題、もう一度手動で名前付けさせる必要は無い。
インストール後、自問した:Amp で作業することもたまにある、その部分は記録される?答えは hook 層分けの話の通り:
| シナリオ | Claude Code hook | git hook | ノートに何が落ちる |
|---|---|---|---|
| Claude Code で作業 + commit | ✅ 発火 | ✅ 発火 | session 詳細 + commit 骨組み |
| Amp で作業 + commit | ❌ 無効 | ✅ 発火 | commit 骨組みのみ |
| 手打ち + commit | ❌ 無効 | ✅ 発火 | commit 骨組みのみ |
| Claude Code で作業、commit まだ | ✅ 録画開始 | — | session 詳細(commit 項目は次 commit 待ち) |
結論:Claude Code を主力にする分には十分。commit 骨組みは全ケースで得られる、session 詳細は Claude Code 経路でのみ得られる。「何をしたか」類の記事は主に commit body に頼る、session の prompt / bash 流はプラスアルファ——あれば良いが必須ではない。
Amp を重く使うなら、Amp には独自の hook 機構がある(詳細は深掘りしていない)、recording-state maybe-start/maybe-stop を発火させる小さな転送スクリプトで同じ動作ができる。
Claude Code session recording を別プロジェクトに移植する 5 ステップ:
cp してターゲットプロジェクトの bin/ へ。無修正——スクリプトは $CLAUDE_PROJECT_DIR を尊重、設計上プロジェクト跨ぎ可搬。.claude/settings.local.json を新規作成、hooks 部分だけ。permissions リストは移植しない——permissions はプロジェクトの状態(プロジェクト毎に違う)、hooks はパターン(プロジェクト跨いで同じ)。.git/hooks/post-commit を新規作成(3 行)、手動で export CLAUDE_PROJECT_DIR=$ROOT して recording-state commit を呼ぶ。ここが git 世界と Claude Code 世界を同じ環境変数で橋渡しする唯一の点。.gitignore に docs/notes/ 追加。ノートは一時的 + プライベート、リポジトリの一部ではない。CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start してブランチ→feature 推論が正しいか確認。tree が clean なら、スクリプトは設計通りスキップ——バグではない。本当の設計判断は「どう移植するか」ではない——移植はほぼ cp だ。録画ロジックを 4 つの異なる層に分割、各層がはっきりした一つの仕事をすることだ:
CLAUDE_PROJECT_DIR を尊重この 4 層はそれぞれ独立に移植・置換・省略できる(Amp サポート欲しい?Amp hook 層を追加。ノート形式変更?Python を編集。git にノート追跡させたくない?post-commit を削除)。Claude はコードを正しく書ける——でも「この機能はどの層に属するか」の判断はあなたの代わりにしてくれない。それはツール境界に関するあなた自身の判断だ、あなたのものだ。