Події hooks
Claude Code запускає команди hooks у певні моменти завдання: надіслано повідомлення, інструмент ось-ось запуститься, інструмент закінчив, завдання завершилося і так далі. aipager чіпляється за кожен із цих моментів і перетворює їх на дії в Telegram: робочу картку, запити на дозвіл, відповіді, сповіщення і журнал аудиту.
Як це підключено#
aipager config (або aipager setup) додає записи aipager у ~/.claude/settings.json, а aipager перевіряє їх під час кожного запуску:
- одна команда
aipager-hookна кожну з 16 подій нижче, з фільтром*на подіях інструментів (PreToolUse,PostToolUse,PermissionRequest); - тайм-аут 5 секунд на
PreModelSwitchі, зaipager configтаaipager setup, 30 секунд наPermissionRequest(сам hook чекає не більше 20 секунд, див. нижче); решта записів використовує значення Claude Code за замовчуванням; - команда
statusLine,aipager-statusline.
aipager uninstall знову прибирає ці записи (див. команди).
Claude Code запускає новий процес aipager-hook (aipager.dtach.notify_hook:main) на кожну подію. Він читає JSON події зі stdin, додає два поля (нижче) і надсилає його aipager однією датаграмою через Unix-сокет. Сокет, по черзі: $AIPAGER_INSTANCE_DIR/aipager.sock для ізольованого тестового екземпляра, $AIPAGER_SOCKET_PATH, $XDG_RUNTIME_DIR/aipager.sock або /tmp/aipager.sock. Надсилання ніколи не чекає: якщо aipager не слухає, подія губиться, а монітор сесій надолужує за файлами сесії.
Більшість подій коштують Claude Code близько 20-25 мс на запуск процесу. Деякі ще й завантажують код правил aipager: перед викликом інструмента (перевірка безпеки, див. PreToolUse) близько 70 мс, у кінці завдання (щоб зняти позначку відкритого завдання) близько 55 мс, і UserPromptSubmit (який записує правила завдання). PermissionRequest навмисно чекає на твоє натискання, до 20 секунд.
Hook обмежує свою пам'ять 1 ГБ. Hook, який упирається в ліміт, завершується з помилкою (для PreToolUse з кодом виходу, який Claude Code читає як відмову) і надсилає aipager датаграму hook_memory_cap_hit, яка перетворюється на сповіщення в чаті з кнопкою «🐞 Повідомити про це» для власника встановлення (див. звіти про проблеми). Задай AIPAGER_DEBUG=1 в оточенні сесії, щоб hook і рядок стану виводили свою діагностику в stderr.
Що отримує aipager#
Датаграма - це власний JSON hook від Claude Code з доданими полями aipager (деякі події несуть більше, це вказано біля кожної події нижче):
| Поле | Значення |
|---|---|
hook_event_name | Подія (поле Claude Code; за ним aipager розподіляє роботу). |
session_id, transcript_path, cwd, ... | Власні поля Claude Code, як прийшли. session_id - це ID розмови в Claude Code. |
session | Додано aipager: його назва для сесії, claude-<name>, зі змінної CLAUDE_DTACH_SESSION, яку aipager задає, коли запускає сесію. Події із сесії Claude Code, яку aipager не запускав, приходять без назви, і aipager їх ігнорує. |
sl_tokens | Додано aipager, на кожній події, крім MessageDisplay, коли існує файл рядка стану сесії: context_pct, total_output, total_input, current_output, lines_added і lines_removed. |
Події інструментів несуть ще tool_name і tool_input. Кожна подія всередині субагента несе agent_id (а події субагентів ще й agent_type); у власних подій основного циклу їх немає, і aipager за цією ознакою ставить виклики інструментів агента на рядок цього агента.
Довідник подій#
Обробники aipager лежать в aipager/dtach/hook_receiver.py; власна робота hook в aipager/dtach/notify_hook.py.
SessionStart#
Сесія запустилася, продовжилася або її розмову стиснуто. Сесія, яку aipager ще не відстежує, реєструється. Після стиснення картка показує 📦 name · Розмову стиснуто: 82% → 4% (див. PreCompact). Для нової розмови hook ще й знімає позначку відкритого завдання з попереднього завдання.
SessionEnd#
Сесія завершилася. Вона позначається як GONE: зникає із закріпленого рядка стану й переходить у завершені сесії, звідки її може повернути /resume. aipager session <name> або /new <name> запускає нову.
UserPromptSubmit#
Claude забрав повідомлення, набране в терміналі або надіслане з Telegram.
Спершу hook звіряє повідомлення з примітками про дозволи сесії (/tmp/claude-notes-<session>/, по одній на кожне повідомлення Telegram, яке ввів aipager) і записує правила завдання в /tmp/claude-policy-<session>.json, які читає перевірка PreToolUse (див. безпека → як правила діють у команді). Він повідомляє aipager, які повідомлення забрав Claude (датаграма queue_pickup), перш ніж передати подію. Потім виводить додатковий контекст для Claude, якщо він є: стиль відповіді, вибраний у /settings, і, під час відповіді на старіше повідомлення, що в ньому було і коли. Повідомлення <task-notification> (Claude будить сам себе, коли фоновий агент або команда закінчили) - це продовження тієї самої роботи: воно не збігається з жодною приміткою і зберігає правила завдання.
aipager позначає сесію як таку, що працює, відкриває робочу картку, якщо її немає (у повідомлення з Telegram вона вже є), і змінює 👀 на повідомленнях, які забрав Claude, на 👍. Повідомлення, надіслане під час завдання, що триває, отримує 👍, коли Claude включає його в це завдання або починає з ним наступне.
PreToolUse#
Інструмент ось-ось запуститься. Тут працює власна перевірка безпеки aipager, усередині hook, до того як Claude Code погляне на свої правила дозволів:
- Завдання, почате з Telegram, тримається правил ролі того, хто його надіслав, і нижнього порогу безпеки (захищені шляхи, шаблони команд), який обходить лише роль
owner; завдання, яке aipager не може ні до кого віднести, отримує поріг; повідомлення, набране тобою в терміналі, не обмежене. Правила описано в групах і безпеці. - Коли правило відхиляє виклик, hook виводить рішення
deny, Claude Code відхиляє інструмент без запитань, а aipager дізнається про це (safety_blocked): завдання зупиняється з🛑 x1 · Заблоковано правилом безпеки: ... (зупинена), і кожен наступний виклик інструмента в цьому завданні теж відхиляється. - Виклик
SendUserFile(Claude надсилає файли в чат) перевіряється для кожного завдання, зокрема завдань owner і повідомлення, набраного в терміналі: нижня межа надсилання, перевірки файлу й правила самого завдання (безпека). Відхилений виклик отримуєdenyбез позначки «aipager safety policy»: Claude читає причину й каже тобі, завдання триває далі, а хук надсилає aipagersend_files_refusedз кожним файлом, для журналу аудиту. - Перевірка, яка сама зламалася, відхиляє, якщо лише правила сесії не вдалося прочитати і вони не дають винятку власника.
На боці aipager виклик стає рядком на робочій картці (виклик субагента потрапляє на рядок цього агента), а Write або Edit показує попередній перегляд змін, коли увімкнено /settings → Попередній перегляд змін (за замовчуванням вимкнено). Виклик AskUserQuestion перетворюється на питання з кнопкою на кожен варіант; питання, яке ставить допоміжний агент Claude Code, коли жодне завдання не триває, не показується (це не Claude питає тебе). Власний виклик Agent у Claude залишає свої subagent_type і description для агента, якого він запускає, а виклик субагента - це остання дія цього агента: і те, і те видно в стані під відповіддю, поки агент працює у фоні.
PermissionRequest#
Claude Code збирається спитати дозволу. Робоча картка стає запитом, зі справжньою командою або шляхом під описом, який дає Claude (див. команди → запити на дозвіл): Дозволити, Відхилити, Зупинити, а Дозволяти завжди лише тоді, коли Claude Code пропонує справжнє правило, яке можна запам'ятати.
Hook відповідає на запит сам, коли може. Він відкриває короткочасний сокет відповіді, надсилає його адресу разом із подією і чекає на твоє натискання до 20 секунд. Вчасне натискання повертається як рішення PermissionRequest (дозволити, дозволити з правилом або відхилити), тож Claude Code свого діалогу не показує. Якщо натискання не прийшло вчасно, hook повідомляє aipager, що очікування закінчилося (permission_reply_timeout), і нічого не виводить; він нічого не виводить і тоді, коли до aipager не достукатися або на цьому шляху щось зламалося. Тоді Claude Code показує свій діалог у терміналі, а пізніше натискання відповідає на нього, вводячи клавіші. Журнал аудиту (~/.claude/aipager-audit.jsonl) записує, яким шляхом відповіло кожне натискання: "via": "hook_decision" або "via": "keystroke_fallback".
PermissionRequest, що прийшов, поки відкрито інший запит, теж показується, окремим повідомленням під робочою карткою. Той самий діалог, що прийшов знову (той самий запитувач, той самий інструмент і виклик або той самий ввід), показується один раз. Claude Code запускає цей hook і для AskUserQuestion; це питання вже показано за його PreToolUse, тому вдруге воно не показується ніколи. Діалог фонового агента з'являється в терміналі лише після того, як його hook перестав чекати, під усіма вже відкритими там діалогами; тому натискання вводить клавіші лише в той запит, який Claude Code показує першим (див. команди → запити на дозвіл).
PostToolUse / PostToolUseFailure#
Інструмент закінчив або впав. Його рядок на робочій картці застигає (помилку позначено на рядку), а відповідь на відкритий запит помічається: старе Дозволити або Відхилити для нього після цього нічого не вводить. Сесія зберігає останні 200 рядків інструментів.
Виклик Bash, який Claude Code переніс у фон, повідомляє backgroundTaskId. aipager тоді відстежує його як фонову команду (лише команди основного циклу: виклик з agent_id належить цьому субагенту), доки не побачить її кінець: <task-notification> зі <status> (у повідомленні, яке будить Claude, або в історії розмови, коли воно приходить посеред завдання), TaskStop або KillShell з її ID, завершення сесії або дві години (AIPAGER_BG_SHELL_MAX_TRACK, у секундах, за замовчуванням 7200). Про її рядок на картці див. команди.
Виклик Agent (або Task) усередині субагента (його подія несе agent_id) повідомляє ID агента, якого він запустив. aipager запам'ятовує, який агент якого запустив, і так відрізняє агента, що зупинився, щоб почекати на свого агента, від того, що закінчив (див. SubagentStart / SubagentStop нижче).
Після виконаного SendUserFile aipager одразу копіює його файли, по разу на виклик і лише під час завдання, що почалося з промпту, за списком із його PreToolUse (ніколи з результату інструмента), знову перевіряючи кожен; копії надсилаються після відповіді завдання, коли виклик знайдено в історії розмови, яку aipager запам'ятав для завдання, і записано під час цього завдання (безпека, команди). Завдання, яке Claude Code починає без хука (повідомлення, яке він бере наступним, відповідь Stop-хука), зараховується, щойно історія розмови показує його початок.
Notification#
aipager читає notification_type від Claude Code:
permission_prompt: повільніший і старіший сигнал запиту на дозвіл. Він показує запит, лише якщо жоден запит не відкритий: якщо якийсь відкритий, це той діалог, який уже показавPermissionRequest.idle_prompt, поки чекає запит на дозвіл або питання: сесія лишається такою, що чекає, а чат отримує⬆️ jim · досі чекає на твою відповідь вище, один раз за очікування.idle_promptв інших випадках: обробляється якStop(нижче). Він підхоплює завдання, чийStopзагубився; уже доставлену відповідь ніколи не надсилають знову.
Stop#
Завдання закінчилося. Сесія стає готовою, робоча картка застигає так, як каже вибраний вигляд, і надсилається відповідь Claude (з last_assistant_message події або інакше з історії розмови). Потім ідуть повідомлення, які притримувалися до кінця завдання. Фонові агенти й команди, які ще працюють, тримають роботу відкритою: картка лишається станом роботи, а відповідь закінчується живим станом цієї роботи: що робить кожен агент і скільки фонових команд роботи триває (див. команди → агенти, які ще працюють). Спершу hook знімає позначку відкритого завдання, якщо лише в Claude немає повідомлення в черзі, яке одразу стає наступним завданням.
StopFailure#
Завдання закінчилося помилкою API. Сесія стає готовою, а відповідь, якщо вона є, доставляється як під час Stop; відома помилка (перевантаження, ліміт запитів, ...) показується простими словами.
SubagentStart / SubagentStop#
Субагент запустився або закінчив. Кожен має свій рядок на робочій картці, 🤖 <type> · <activity> · <elapsed>, з його власними викликами інструментів, згорнутими під ним, і під час зупинки він застигає як ✅ 🤖 <type> · N інструментів · <elapsed>. SubagentStart не каже, який агент його запустив: aipager зіставляє його з викликом Agent (PreToolUse) того самого типу, що чекає. Якщо всі такі виклики прийшли від одного агента, новий агент одразу прив'язується до цього агента (його рядок на картці виглядає як 🤖 pipeline-runner › ship-tester, а в статусі під відповіддю він стоїть під цим агентом); якщо від різних агентів, нічого не прив'язується, доки власний PostToolUse агента, що його запустив, не назве його. Опис виклику використовується, лише коли чекає рівно один виклик цього типу. Субагент, чий SubagentStop так і не прийшов, прибирається, коли він мовчить 30 хвилин (AIPAGER_SUBAGENT_SILENCE, у секундах; жодної події будь-якого виду з його ID). Агент, що працює, надсилає події інструментів кожні кілька секунд, тому так прибирається лише агент, чию зупинку пропустили (перезапуск, загублена датаграма); один виклик інструмента, що триває 30 хвилин без жодної події під ID цього агента, теж його прибирає. AIPAGER_SUBAGENT_TTL приймається як стара назва того самого налаштування. Події SubagentStop без типу і з невідомим ID приходять часто й ігноруються.
Фоновий агент, який зупиняється, поки запущений ним агент ще працює, теж отримує SubagentStop. aipager вважає це паузою, а не кінцем: агент лишається таким, що працює (його рядок, робота, стан під відповідями, закріплений рядок). Його наступний SubagentStart з тим самим ID означає, що він продовжив, і не додає другого рядка; будь-який його власний виклик інструмента теж вважається продовженням. Він закінчує, коли зупиняється, а жоден із його власних агентів уже не йде. Якщо він не продовжив через дві хвилини після того, як закінчився останній із них, він більше не вважається таким, що працює, але ніколи не показується закінченим. Агенти, що працюють, зокрема ті, що чекають, зберігаються під час перезапуску aipager.
PreCompact / PostCompact#
Claude стискає свій контекст (trigger дорівнює auto або manual). Робоча картка показує 🔄 name · Стискає розмову (нове повідомлення, якщо картки немає), щоб пауза не виглядала як збій. PostCompact завершує цей стан; рядок 📦 name · Розмову стиснуто: X% → Y% приходить із наступного SessionStart або, якщо його пропущено, з рядка стану. Рядок стану ще й дає попередження ⚠️ name · Контекст заповнено на 82% - скоро стиснеться автоматично (з кнопкою Стиснути), коли контекст доходить до 80 %, один раз за розмову: стискання його не повторює, а нова розмова (наприклад, після /clear) отримує його знову.
MessageDisplay#
Власний текст Claude в момент, коли він з'являється на екрані. Історія розмови пишеться лише після кожного раунду викликів інструментів, тому це єдине актуальне джерело того, що каже Claude, і саме воно передає його коментарі в робочу картку. Текст субагента (будь-яка подія з його agent_id) ніколи не приймається за текст Claude: він не потрапляє ні в коментарі робочої картки, ні в Mini App, ні в журнал завдання і стає останньою дією цього агента в стані під відповіддю. Hook не додає до цієї події даних рядка стану: Claude Code чекає на неї, поки малює, і вона приходить кілька разів за повідомлення.
PreModelSwitch#
Claude Code 2.1.251 і новіші викликають його перед зміною моделі. Вміст несе from_model, to_model, requested_model і source (command для набраного /model, або picker чи sdk). Перед зміною Claude Code зазвичай питає "Switch model?", бо наступна відповідь має перечитати всю розмову. Hook, який відповідає permissionDecision: "allow", пропускає зміну без цього питання.
aipager-hook відповідає allow лише на зміну, яку ввів сам aipager, з перемикача моделі в Mini App або з клавіатури моделей. Будь-яка інша зміна не отримує відповіді, тому Claude Code питає, як звичайно. Це стосується /model, який оператор набирає в терміналі, вибору в списку і зміни через SDK.
aipager упізнає свої зміни за файлом-позначкою. Просто перед тим, як ввести /model <name>, aipager пише aipager-modelswitch-<session>.json:
- він лежить у папці, де лежить керувальний сокет (зазвичай
$XDG_RUNTIME_DIR); - він пишеться атомарно, з правами 0600;
- у ньому вказано модель та ID сесії Claude;
- він спливає через 30 секунд.
Hook відповідає allow, лише якщо виконано все це:
- позначка - це файл, що належить тобі, і не символічне посилання;
- вона не спливла;
- вона для тієї самої сесії aipager і того самого ID сесії Claude;
- її модель дорівнює
requested_modelабоto_modelбез урахування регістру; sourceдорівнюєcommand.
Потім hook забирає позначку атомарним перейменуванням, щоб вона спрацювала рівно один раз. Перевірка читає один локальний файл, без сокета й без очікування. Подія не передається aipager.
Claude Code 2.1.101 і новіші ігнорують незнайомі події hooks, тому старіших версій без PreModelSwitch це не зачіпає. Один випадок усе ще може показати питання: зміна на opusplan або з нього може змусити Claude Code спитати hook двічі, і лише перше питання знаходить позначку, тож Claude Code питає "Switch model?" у терміналі.
Рядок стану#
Це не подія hook: Claude Code запускає команду statusLine, aipager-statusline (aipager.dtach.statusline_notify:main), щоразу, коли перемальовує свій рядок стану. Команда:
- записує JSON стану Claude Code у
/tmp/claude-status-<session>.json(його читають hook дляsl_tokens,aipager statusі aipager); - надсилає aipager датаграму
statuslineз моделлю, % контексту, вартістю, кількістю токенів і доданими та видаленими рядками, які йдуть у робочу картку,/status, Mini App, перевірку того, що зміна моделі пройшла, і попередження про контекст; - виводить рядок стану термінала:
[name] Opus 5.5 | 42% ctx | $1.23.
Власні датаграми aipager#
Крім подій hooks, допоміжні програми надсилають aipager кілька своїх повідомлень через той самий сокет:
| Повідомлення | Хто надсилає | Коли |
|---|---|---|
queue_pickup | hook | Під час UserPromptSubmit: які повідомлення Telegram забрав Claude. |
safety_blocked | hook | Під час PreToolUse, коли перевірка безпеки відхилила виклик. |
send_files_refused | хук | Під час PreToolUse, коли файли, які Claude хотів надіслати, відхилено: кожен файл і причина, для журналу аудиту. |
permission_reply_timeout | hook | Очікування PermissionRequest закінчилося без натискання. |
statusline | рядок стану | Під час кожного перемальовування рядка стану. |
hook_memory_cap_hit | hook, рядок стану | Процес уперся у свій ліміт пам'яті. |
report_error | hook, команди aipager | Сталася помилка: лише тип помилки та її місце в коді aipager, ніколи не вміст події, для звітів про проблеми. |
Шлях запиту на дозвіл#
claude aipager-hook aipager daemon Telegram
| | | |
| PermissionRequest | | |
|--------------------->| opens a reply socket | |
| |------------------------>| busy card becomes |
| | | the prompt |
| | |-------------------->|
| | | you tap Allow |
| | |<--------------------|
| | allow (reply socket) | audit log |
| |<------------------------| |
| decision: allow | | |
|<---------------------| | |
| tool runs | | |
Якщо натискання немає 20 секунд, hook нічого не виводить, і Claude Code показує свій діалог; пізніше натискання вводить відповідь у цей діалог через термінал сесії (dtach -p). В обох випадках натискання записується в журнал аудиту й публікується під карткою (✅ jim · Дозволено · @alice · Bash: ls -la /tmp).
Див. також#
- Архітектура: де лежить приймач hooks.
- Команди бота → запити на дозвіл: бік, який бачиш ти.
- Модель безпеки: що перевірка безпеки покриває і що ні.