免费

让 Claude 把 session recording 搬到第二个项目

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 个文件一张图

把 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 分层决定什么能捕获

两种 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、作者、分支、diff)

实际后果:你用 Claude Code 写代码 + commit,两层 hook 都会开火,session 信息和 commit 信息都进 raw.md。你改用 Amp(或 Cursor、或手敲)写代码 + commit,只有 git hook 会开火,raw.md 只留下 commit 骨架,没有 session 的 prompt / bash / edit 细节。

这不是 bug——是这两层工具各自的设计约束。想在所有工具下都拿到 session 细节,你得在每个工具里都装一层自己的 hook。单靠 git 兜底拿到的是"做了什么",但不是"是怎么想的、踩了哪些坑"。

选型指导
- 想要工具无关的骨架(commit 信息、代码变更)→ 放 git hook
- 想要 Claude Code 特有的血肉(完整 prompt、思考过程)→ 放 Claude Code hook
- 两者都要 → 两层都放

迁移过程:动的就这 5 步

给 smarts(/home/bob/Work/smarts,一个 Rails 项目)装上同样的录制系统。

1. 拷贝两个 Python 脚本

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,不搬权限列表

how2claude 的 settings.local.json 里有 100 多条 permissions.allow——全是针对 how2claude 项目的(curl localhost:3000、bin/rails runner、kamal app exec)。这些搬到 smarts 毫无意义。smarts 会在用的过程中自然累积自己的权限列表。

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" }
        ]
      }
    ]
  }
}

三个触发点:
- 文件被编辑 → 尝试起录(maybe-start 会自检:已录中就跳过、tree clean 也跳过)
- bash 命令执行 → 尝试停录(maybe-stop 严格:只有"自动起的 + 回到 master + tree clean"三条件同时满足才停)
- 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——这就是把 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 跟着 commit 进 PR;你也不希望 .state.json 污染 git 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——自动起了录,feature 名从分支 feat/contract-to-docs 推导出 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(落回 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 把 session recording 搬到另一个项目的 5 步:

  1. cp 两个 Python 脚本到目标项目的 bin/——零修改,脚本认 $CLAUDE_PROJECT_DIR,跨项目天然可移植。
  2. 新建 .claude/settings.local.json,只放 hooks 部分。不搬权限列表——permissions 是项目状态,跨项目各异;hooks 是模式,跨项目一样。
  3. 新建 .git/hooks/post-commit(3 行),手动 export CLAUDE_PROJECT_DIR=$ROOTrecording-state commit。这是 git 和 Claude Code 两层靠同一环境变量接上的地方。
  4. .gitignoredocs/notes/。笔记是临时 + 私有,不入 git。
  5. 手动 smoke-testCLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start 看分支推断对不对。如果 tree 不是 dirty,脚本按设计会跳过——不是 bug。

真正的设计决定不是"怎么搬"——几乎就是 cp。是把录制逻辑拆在 4 个不同层,每层做一件清晰的事

  • Python 脚本:无状态 helper,只认 CLAUDE_PROJECT_DIR
  • Claude Code hook:工具内事件(session 血肉)
  • git hook:工具无关事件(commit 骨架)
  • gitignore:噪音控制

这 4 层分别可以迁移、分别可以替换(把 Amp 当 Claude Code 用?只需补一层 Amp hook;换一个笔记格式?只改 Python 脚本;不想 git 追笔记?删 post-commit 就行)。Claude 把代码写对是容易的事,但"该把功能切在哪一层"这个决定不会有人替你做——那是你自己对工具边界的清晰判断。