Free

Нехай Claude пише хуки, що записують його самого

Найважче у статтях-розборах — зібрати матеріал: у 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. Ця — теж.


Огляд 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. Усі хуки читають і пишуть у нього. Іншої координації нема.

Хук №1: коли починати

"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: писати код — це справжній сигнал «я справді щось роблю». Читати файли, ганяти тести, ставити питання — не рахується.

Хук №2: коли зупинятися

{
  "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.

Хук №3: commit як checkpoint

Не 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, він по суті пише матеріал-резюме для твоєї майбутньої статті — потрібно лише, щоб він зробив добре з першого разу, без переписування.

Хук №4: витягти події з transcript

"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: використай курсор

Хук 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)

Кожен прохід бере лише події після курсора. Просто, але забудь — і посиплються сотні дубльованих рядків.

Ручне перевизначення: дві slash-команди

/record-feature NAME:

{
  "feature": "NAME",
  "started_at": "ISO timestamp",
  "branch": "current branch",
  "auto_started": false
}

Рядок auto_started: false вимикає auto-stop. Сценарії: пишеш одноразовий скрипт на master, але хочеш залишити слід; фіча, що перетинає кілька гілок; явно заявити «ця під статтю».

/stop-recording: проганяє фінальне витягання по найсвіжішому jsonl, потім чистить файл стану.

Як виглядає raw.md

Реальний фрагмент (із 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 писати хуки, що записують його самого.