Free

Claude Code の session recording を 2 つ目のプロジェクトに移植する

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)に何が捕捉され何が漏れるかは、どの層に置いたかで決まる。


4 ファイル、1 枚の絵

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 つの異なる層に座っている。この区別は以降で繰り返し使う。

核心の洞察:hook の層分けが何を捕捉するかを決める

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 に置く
- 両方欲しい → 両層に置く

移植手順:動くのはこの 5 ステップだけ

smarts(/home/bob/Work/smarts、Rails プロジェクト)に同じ録画システムを仕込む。

1. Python スクリプト 2 本をコピー

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 では手動設定——両側が同じ環境変数を尊重、スクリプトは「自分がどのプロジェクトにいる」を判定する必要が無い。

2. .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 に書く

3. .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

4. .gitignoredocs/notes/ 追加

# Session recording notes (transient, for article material)
docs/notes/

ノートは一時的 + プライベート——raw.md を PR に commit したくない、.state.jsongit status を汚したくない。how2claude の gitignore も同じ処理。

5. 煙テスト(小さな驚き付き)

インストール直後に 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/XXfeature/XX、素の fix-yfix-y、master/main → None(これは timestamp 付き session-YYYYMMDD-HHMM 名にフォールバック)。

このヒューリスティックはちょうど良い無脳さ——ブランチ名こそが作業の主題、もう一度手動で名前付けさせる必要は無い。

Amp 混用時の可視性境界

インストール後、自問した: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 ステップ:

  1. Python スクリプト 2 本を cp してターゲットプロジェクトの bin/ へ。無修正——スクリプトは $CLAUDE_PROJECT_DIR を尊重、設計上プロジェクト跨ぎ可搬。
  2. .claude/settings.local.json を新規作成、hooks 部分だけ。permissions リストは移植しない——permissions はプロジェクトの状態(プロジェクト毎に違う)、hooks はパターン(プロジェクト跨いで同じ)。
  3. .git/hooks/post-commit を新規作成(3 行)、手動で export CLAUDE_PROJECT_DIR=$ROOT して recording-state commit を呼ぶ。ここが git 世界と Claude Code 世界を同じ環境変数で橋渡しする唯一の点。
  4. .gitignoredocs/notes/ 追加。ノートは一時的 + プライベート、リポジトリの一部ではない。
  5. 手動煙テストCLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start してブランチ→feature 推論が正しいか確認。tree が clean なら、スクリプトは設計通りスキップ——バグではない。

本当の設計判断は「どう移植するか」ではない——移植はほぼ cp だ。録画ロジックを 4 つの異なる層に分割、各層がはっきりした一つの仕事をすることだ:

  • Python スクリプト:状態を持たない helper、CLAUDE_PROJECT_DIR を尊重
  • Claude Code hook:ツール内イベント(session の肉)
  • git hook:ツール非依存イベント(commit の骨組み)
  • gitignore:ノイズ制御

この 4 層はそれぞれ独立に移植・置換・省略できる(Amp サポート欲しい?Amp hook 層を追加。ノート形式変更?Python を編集。git にノート追跡させたくない?post-commit を削除)。Claude はコードを正しく書ける——でも「この機能はどの層に属するか」の判断はあなたの代わりにしてくれない。それはツール境界に関するあなた自身の判断だ、あなたのものだ。