Free

Claude Code 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, 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" }
        ]
      }
    ]
  }
}

트리거 포인트 셋:
- 파일 편집됨 → 녹화 시작 시도(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 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인 걸 정확히 식별—자동으로 녹화 시작, 브랜치 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/XX, feature/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는 코드를 정확히 쓸 수 있지만—"이 기능은 어느 레이어에 속하나"의 판단은 네 대신 해주지 않음. 그건 네 도구 경계에 관한 판단이고, 네 거임.