Сложнейшее в статьях-разборах — собрать материал: в git log только commit message, детали сессии теряются. Четыре хука и две slash-команды превращают каждую сессию в raw.md — предельные издержки ноль.
Самое сложное при написании статьи "Claude сделал за меня X" — не сама писанина, а сбор материала. В git log только commit message; отвергнутые варианты, провальные попытки, три переписанные версии того промпта, вывод ошибки — это и есть то, ради чего статью читают, и всё это ушло.
Хуже: к моменту, когда вы садитесь писать, вы уже не помните, почему выбрали A, а не B. "Помню, была причина" и "эта причина — X" — разница между убедительной статьёй и водой.
Протоколы встреч стоят дороже, чем статья целиком, поэтому вы их не ведёте. Единственное решение — пусть запись происходит сама — хуки.
Я заставил Claude написать 4 хука + 2 slash-команды, которые превращают каждую сессию разработки в docs/notes/<feature>/raw.md. В предыдущей статье Отдаём продакшен-деплой Claude большая часть цитируемых коммитов, фрагментов bash и ссылок на ошибки пришла из этого pipeline. Эта — тоже.
4 хука + 2 ручные slash-команды формируют одну конвейерную ленту:
| Триггер | Что делает |
|---|---|
| PostToolUse(Edit\ | Write\ |
| PostToolUse(Bash) | Останавливает при возврате на master + чисто |
| git post-commit | На каждый коммит пишет checkpoint |
| Stop | Парсит transcript в конце сессии |
/record-feature NAME /stop-recording |
Ручная перебивка |
Один файл состояния: docs/notes/.state.json. Все хуки читают и пишут в него. Больше координации нет.
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
}]
}
]
Ядро maybe-start:
def cmd_maybe_start():
if load_state(): return # уже записываем
if not tree_is_dirty(): return # ничего не поменялось, пропуск
feature = branch_to_feature(current_branch())
if not feature:
feature = "session-" + datetime.now().strftime("%Y%m%d-%H%M")
save_state({"feature": feature, "auto_started": True, ...})
branch_to_feature("feature/pro") возвращает "pro". Разработчики и так делят работу по веткам, так что переиспользуем эту ментальную границу — коренная причина, по которой pipeline выживает: он не просит пользователя запоминать ничего нового. На master одноразовые скрипты откатываются к timestamp.
Почему matcher — Edit/Write/MultiEdit: писать код — это настоящий сигнал "я правда что-то делаю". Читать файлы, гонять тесты, задавать вопросы — не считается.
{
"matcher": "Bash",
"hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop" }]
}
def cmd_maybe_stop():
state = load_state()
if not state or not state.get("auto_started"): return
if current_branch() not in ("master", "main"): return
if tree_is_dirty(): return
clear_state()
Три шлюза: ручные записи не останавливаются, feature-ветки не останавливаются, грязное дерево не останавливается.
Почему повешен на Bash, а не на Edit: после мержа в master обычно уже не редактируешь, но точно запустишь git status / git log / bin/rails test — любая команда даёт хуку шанс заметить "пора закругляться".
Флаг auto_started — несущий элемент. Если ты запустил /record-feature pro-launch и прошёл несколько веток через несколько мержей, правила auto-stop прикончили бы запись посреди фичи. Ручные записи никогда не останавливаются автоматически — только /stop-recording.
Не Claude Code хук — это .git/hooks/post-commit:
#!/bin/bash
ROOT=$(git rev-parse --show-toplevel) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true
recording-state commit дописывает полное сообщение коммита в raw.md:
### Commit 2026-04-16 21:55: `71f38a1`
> Add pricing page and expand account UI (P6 phases 1-2)
>
> Pricing (/pricing):
> - Displays all 6 plans in monthly/yearly grid with Stimulus toggle
> - Anonymous users see Subscribe → sign-in flow
> ...
Почему commit message важны: это написанное вручную резюме, которое ты (или Claude) произвёл ровно в момент, когда фича только что закончена, а контекст полон. Точнее, чем вспоминать потом, короче transcript'а, абстрактнее diff'а.
Когда Claude пишет commit message, он по сути пишет материал-резюме для твоей будущей статьи — тебе нужно только, чтобы он сделал это хорошо с первого раза, без переписывания потом.
"Stop": [{
"hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }]
}]
Когда сессия заканчивается, Claude Code передаёт transcript_path в хук через stdin как JSON. extract-session-notes открывает этот jsonl, идёт по нему строка за строкой и раскладывает по вёдрам:
keep_patterns = (
"test", "spec", "rspec", "minitest",
"kamal", "git commit", "git push",
"rails db", "rails routes", "rails runner",
"migrate", "curl -X", "curl -s -X",
)
if any(kw in cmd for kw in keep_patterns):
bash_cmds.append({"cmd": cmd[:400], "desc": desc})
Захватываются только bash-команды из whitelist. 90% bash в сессии кода — это ls / cat / grep / head — в статьях не используются, отфильтровываем всё. Оставшееся (запуск тестов, kamal, rails runner, curl по API) — команды с историей.
Пользовательские промпты в обёртках <command-*> или <system-*> выбрасываются — выживает только настоящий ввод. Пути Edit/Write дедуплицируются в set. Вывод ошибок обрезается до 400 символов. Вызовы Task sub-agent сохраняются (промпт обрезается до 2000 символов — достаточно, чтобы увидеть, что sub-agent сделал).
Хук Stop срабатывает не один раз на запись — он срабатывает каждый раз, когда сессия заканчивается. На одной и той же feature-ветке ты можешь открыть и закрыть Claude Code пять-шесть раз. Если каждый Stop будет пере-парсить весь transcript и писать всё, raw.md утонет в дубликатах.
Решение: поставить курсор last_extracted_at в файле состояния:
filter_after = state.get("last_extracted_at") or state.get("started_at")
events = extract_events(transcript_path, filter_after)
# ...после записи...
state["last_extracted_at"] = datetime.now().astimezone().isoformat()
save_state(project_dir, state)
Каждый проход берёт только события после курсора. Просто, но забудь — и сыплются сотни дублированных строк.
/record-feature NAME:
{
"feature": "NAME",
"started_at": "ISO timestamp",
"branch": "current branch",
"auto_started": false
}
Строка auto_started: false отключает auto-stop. Сценарии: пишешь одноразовый скрипт на master, но хочешь оставить след; фича, пересекающая несколько веток; явно заявить "эта под статью".
/stop-recording: прогоняет финальное извлечение по самому свежему jsonl, затем чистит файл состояния.
Реальный фрагмент (из docs/notes/pro/raw.md):
## Session 2026-04-16 21:52 (`7a81bf9d`)
### User prompts
- 根据环境变量直接写好对应只,不用传。
### Files edited/written
- `config/deploy.yml`
- `config/initializers/x402.rb`
### Commit 2026-04-17 00:13: `f87ea8e`
> Add production credentials (Stripe live + x402 mainnet)
> ...
### Commit 2026-04-17 00:57: `eba9ac9`
> Fix production x402 wallet_address (stray fullwidth '?' at end)
Чтобы написать статью: просто grep. "Какой коммит был фиксом wallet address?" → eba9ac9. "Как я промптил sub-agent рефакторинга?" → под ### Sub-agent invocations.
docs/notes/ в .gitignore — это черновой материал для писанины, не исходный код.
1. Ветка как граница фичи, а не отдельная система метаданных. Разработчики уже делят работу по веткам; добавление ещё одного слоя "имя фичи" неизбежно расползётся. Переиспользовать существующую границу — нулевая когнитивная нагрузка.
2. Whitelist для bash, а не blacklist. 95% bash в transcript'ах — шум. Список "что стоит сохранять" стабилен годами; список "что фильтровать" — нет.
3. Курсор живёт в файле состояния, а не в transcript'е. Transcript принадлежит Claude Code; state принадлежит pipeline. Развязаны — Claude Code может менять формат transcript'а, не ломая меня, а я могу переписывать логику извлечения, не трогая transcript'ы.
Написание статьи на 1500 слов потребляет материал примерно из 30 коммитов, 4 сессий, десятка ключевых bash-команд. Собрать вручную — около часа, хватает, чтобы в следующий раз ты срезал углы.
Пусть хуки делают это: предельные издержки — ноль. Установи это один раз и просто работай как обычно — тяни ветку, пиши код, деплой — и материал для статьи копится сам.
Каждая ссылка на коммит, фрагмент bash, строка ошибки и путь к файлу в этой статье пришли из docs/notes/pro/raw.md — черновой материал, который хук записал сам с момента, когда я два дня назад потянул feature/pro. Когда пришло время писать, я открыл файл — и всё, что нужно, уже было там.
Лучший способ заставить Claude писать статьи про Claude Code — сначала заставить Claude писать хуки, которые записывают его самого.