Найважче у статтях-розборах — зібрати матеріал: у 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 писати хуки, що записують його самого.