Систему запису 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), залежить від того, на якому шарі ти встановив.
Усе в 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 працюють одночасно, але зі зовсім різними 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
- І те, й інше → ставити на обох шарах
Встановлення тієї самої схеми запису в 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}
Скрипти без правок — вони використовують 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-змінну, і скрипту не треба з'ясовувати «в якому я проєкті».
.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
.git/hooks/post-commit3 рядки:
#!/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
docs/notes/ у .gitignore# Session recording notes (transient, for article material)
docs/notes/
Нотатки тимчасові + приватні — не хочеться, щоб raw.md потрапив у PR; не хочеться, щоб .state.json засмічував git status. Gitignore у how2claude поводиться з цим так само.
Одразу після встановлення вручну смикнути 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/X → X, feature/X → X, голий fix-y → fix-y, master/main → None (відкочується на ім'я з таймстемпом session-YYYYMMDD-HHMM).
Евристика навмисно тупенька в міру — ім'я branch і є темою роботи, нема потреби змушувати тебе називати знов.
Після встановлення я запитав себе: іноді працюю в 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 рухів:
cp двох Python-скриптів у 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 з'єднуються однією env-змінною.docs/notes/ у .gitignore. Нотатки тимчасові + приватні, не частина репо.CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, перевірити, що вивід branch→feature правильний. Якщо tree clean, скрипт за дизайном пропускає — це не баг.Справжнє дизайн-рішення не «як перенести» — перенос це майже cp. Воно в тому, щоб розрізати логіку запису на 4 різні шари, кожен із яких робить одну ясну річ:
CLAUDE_PROJECT_DIRКожен із 4 шарів можна незалежно переносити, замінювати або пропускати (потрібна підтримка Amp? додай Amp-шар. Змінити формат нотаток? правь Python. Не хочеш, щоб git відстежував нотатки? видали post-commit). Claude може написати код правильно — але судження «до якого шару належить ця функціональність» він за тебе не винесе. Це судження про межі твого інструментарію, і воно твоє.