Free

Перенос session recording Claude Code у другий проєкт

Систему запису how2claude переносимо в інший Rails-проєкт: 4 файли, 5 кроків, шари hook.


У how2claude є набір hooks, які автоматично записують сесії Claude Code — стартують запис при початку роботи, додають checkpoint при кожному commit, а на завершенні сесії виймають prompts / bash / списки правок у docs/notes/<feature>/raw.md. Стаття let-claude-record-itself розповіла, як це побудовано.

Проблема: мій інший проєкт (smarts, сайт документації для смартконтрактів) нічого цього не мав. Щоразу, коли хотілося написати статтю постфактум, я копирсався в git log і пам'яті, відчуваючи, що найцікавіше проходить повз. Ця стаття розказує, як я переніс систему запису туди — усього 4 файли, 5 хвилин — але по дорозі з'явилася справжня думка про шари hooks: hooks Claude Code і hooks git працюють на зовсім різних шарах, і що саме захоплюється при змішуванні інструментів (наприклад Amp + Claude Code), залежить від того, на якому шарі ти встановив.


4 файли, одна картинка

Усе в how2claude, що стосується запису:

Файл Шар Роль
bin/recording-state скрипт Python helper, керує життєвим циклом .state.json
bin/extract-session-notes скрипт Python helper, читає transcript Claude Code → пише в raw.md
.claude/settings.local.json hook Claude Code PostToolUse / Stop тригерять скрипти вище
.git/hooks/post-commit hook git кожен commit викликає recording-state commit для checkpoint
.gitignore контроль шуму тримає docs/notes/ поза репо (нотатки приватні/тимчасові)

4 шматки, 4 різні шари. Ця різниця повернеться нижче.

Ключова думка: шари hooks вирішують, що захоплюється

Дві системи hooks працюють одночасно, але зі зовсім різними scope:

Hooks Claude Code (визначені в .claude/settings.local.json):
- Scope: спрацьовують лише всередині інструменту Claude Code
- Тригери: PostToolUse / Stop / PreToolUse — події життєвого циклу Claude Code
- Доступна інфо: ім'я tool, аргументи, transcript_path (повний jsonl сесії) — речі, відомі лише Claude Code

Hooks git (shell-скрипти під .git/hooks/):
- Scope: спрацьовують на всіх подіях git, не важливо, хто ініціював git
- Тригери: post-commit / pre-push / тощо
- Доступна інфо: те, що знає сам git (sha, автор, branch, diff)

Реальний наслідок: пишеш код + commit усередині Claude Code — спрацьовують обидва шари, інфа сесії та інфа commit потрапляють у raw.md. Перемикаєш на Amp (або Cursor, або руками) для write + commit — спрацьовує лише git hook — raw.md отримує скелет commit, але жодного prompt / bash / деталей правок сесії.

Це не баг — це дизайн-обмеження кожного інструменту. Хочеш деталь рівня сесії під будь-яким інструментом — встановлюй свій шар 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}

Скрипти без правок — вони використовують env-змінну $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 виставляє CLAUDE_PROJECT_DIR автоматично при спрацьовуванні hooks; ми виставляємо її вручну всередині git post-commit. Обидві сторони шанують одну env-змінну, і скрипту не треба з'ясовувати «в якому я проєкті».

2. Створити .claude/settings.local.json

Одне рішення тут: переносимо лише hooks, не список permissions.

У settings.local.json у how2claude 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» одночасно)
- Сесія завершилась → витяг 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

Ручний export CLAUDE_PROJECT_DIR=$ROOT — це точний пункт, де світ git і світ Claude Code з'єднуються однією env-змінною. || true гарантує, що hook ніколи не блокує commit.

chmod +x /home/bob/Work/smarts/.git/hooks/post-commit

4. Додати docs/notes/ у .gitignore

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

Нотатки тимчасові + приватні — не хочеться, щоб raw.md потрапив у PR; не хочеться, щоб .state.json засмічував git status. Gitignore у how2claude поводиться з цим так само.

5. Smoke-тест (з маленьким сюрпризом)

Одразу після встановлення вручну смикнути 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 — автоматично стартонув запис, вивів ім'я feature contract-to-docs з branch feat/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 (відкочується на ім'я з таймстемпом session-YYYYMMDD-HHMM).

Евристика навмисно тупенька в міру — ім'я branch і є темою роботи, нема потреби змушувати тебе називати знов.

Межа видимості при міксі з Amp

Після встановлення я запитав себе: іноді працюю в Amp — та частина захопиться? Відповідь рівно така, як передбачає історія про шари hook:

Сценарій Hook Claude Code Hook git Що потрапляє в нотатки
Робота в Claude Code + commit ✅ спрацьовує ✅ спрацьовує деталі сесії + скелет commit
Робота в Amp + commit ❌ no-op ✅ спрацьовує лише скелет commit
Ручний набір + commit ❌ no-op ✅ спрацьовує лише скелет commit
Робота в Claude Code, commit поки немає ✅ стартує запис деталі сесії (запис commit чекає наступного commit)

Висновок: вистачає, якщо Claude Code — головний водій. Скелет commit потрапляє завжди; деталі сесії — лише маршрутом Claude Code. Для статей «що було зроблено» переважно спираєшся на body commit — prompt / bash-сліди сесії — бонус, добре мати, але не обов'язково.

Якщо Amp використовується інтенсивно, у Amp своя система hook (у деталі не занурювався); маленький скрипт-прошарок, що тригерить recording-state maybe-start/maybe-stop, спрацює так само.

Чек-лист

Перенос session recording Claude Code в інший проєкт — 5 рухів:

  1. cp двох Python-скриптів у 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 з'єднуються однією env-змінною.
  4. Додати docs/notes/ у .gitignore. Нотатки тимчасові + приватні, не частина репо.
  5. Ручний smoke-тест: CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, перевірити, що вивід branch→feature правильний. Якщо tree clean, скрипт за дизайном пропускає — це не баг.

Справжнє дизайн-рішення не «як перенести» — перенос це майже cp. Воно в тому, щоб розрізати логіку запису на 4 різні шари, кожен із яких робить одну ясну річ:

  • Python-скрипти: helper без стану, поважає CLAUDE_PROJECT_DIR
  • Hook Claude Code: події всередині інструменту (м'ясо сесії)
  • Hook git: інструмент-агностичні події (скелет commit)
  • gitignore: контроль шуму

Кожен із 4 шарів можна незалежно переносити, замінювати або пропускати (потрібна підтримка Amp? додай Amp-шар. Змінити формат нотаток? правь Python. Не хочеш, щоб git відстежував нотатки? видали post-commit). Claude може написати код правильно — але судження «до якого шару належить ця функціональність» він за тебе не винесе. Це судження про межі твого інструментарію, і воно твоє.