Систему записи 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 может написать код правильно — но суждение «к какому слою относится эта функциональность» он за тебя не вынесет. Это суждение о границах твоего инструментария, и оно твоё.