Я часто даю Claude Code длинную задачу и ухожу в соседнее окно. Через десять-пятнадцать минут возвращаюсь и вижу, что он давно упёрся в вопрос «можно выполнить эту команду?» и всё это время просто ждал. Системное уведомление Windows, конечно, показывалось, но на пять секунд в углу, пока я смотрел в другую сторону.
Подсказывать себе таймером не хотелось, нужна была штука, которую невозможно не заметить. В итоге получился маскот: маленький оранжевый человечек, который выглядывает из правого нижнего угла экрана, а если его игнорировать, выходит целиком, краснеет и начинает топать по панели задач. Ниже расскажу, как это устроено. Основные сложности оказались не в анимации, а в двух вещах: как вообще узнать, что Claude ждёт, и как нарисовать окно, которое видно поверх всего, но ничему не мешает.

Код открыт, MIT: https://github.com/OlegChe77/claude-mascot. Стек: Electron и немного WinAPI через koffi.
Как узнать, что Claude ждёт
Это главный вопрос, и ответ зависит от того, чем вы пользуетесь.
Claude Code в терминале: хуки
У Claude Code есть хуки: в ~/.claude/settings.json можно описать команду, которую запустят на определённом событии, а на stdin ей придёт JSON с описанием. Нужное событие называется Notification: оно срабатывает, когда Claude хочет привлечь внимание. Хук можно сделать совсем лёгким:
{ "hooks": { "Notification": [ { "hooks": [ { "type": "command", "command": "curl -s -m 2 --data-binary @- http://127.0.0.1:47821/hook || exit 0" } ] } ] }}
Программа слушает HTTP на 127.0.0.1:47821, и хук просто отправляет ей то, что пришло на stdin. Часть || exit 0 нужна для того, чтобы при незапущенном маскоте команда молча завершалась с нулём: иначе у Claude Code на каждом событии вылезала бы ошибка хука. Заодно такая команда одинаково работает и в bash, и в cmd.
Запросы с заголовком Origin сервер отбрасывает. Это защита от того, чтобы произвольная веб-страница в браузере не могла постучаться в локальный порт и вызвать маскота. Ничего критичного там нет, но привычка проверять такие вещи не повредила.
Десктопный Claude: события, которых нет
С терминальной версией всё просто. Десктопное приложение (вкладка Code) устроено хитрее: когда оно просит разрешение на инструмент или задаёт вопрос, событие Notification оно не отправляет. Об этом я узнал не сразу, потому что в терминале всё работало, а на десктопе маскот молчал.
Пришлось добавить ещё два события. PermissionRequest ловит запрос разрешения, а PreToolUse с matcher AskUserQuestion|ExitPlanMode ловит инструменты, которые сами задают вопрос или просят утвердить план. Есть и необязательный Stop, он срабатывает после каждого ответа, кому-то это нужно.
Хуки маскот ставит сам при запуске. Перед записью он кладёт рядом копию settings.json.mascot-*.bak (хранятся пять последних) и не трогает чужие хуки. Ещё нюанс: уже открытые сессии могут не подхватить новые хуки до перезапуска, поэтому для десктопа нужен второй источник сигнала.
Уведомления Windows как источник правды
Второй источник — сами уведомления. Windows пишет каждую всплывающую плашку в SQLite-базу %LOCALAPPDATA%MicrosoftWindowsNotificationswpndatabase.db. Достаточно раз в секунду открывать её на чтение и смотреть, не появилась ли запись с идентификатором приложения вида Claude_*!Claude:
const { DatabaseSync } = require('node:sqlite');const db = new DatabaseSync(DB, { readOnly: true });const ids = db.prepare( "select PrimaryId as id from NotificationHandler where PrimaryId glob 'Claude_*!Claude'").all();
Модуль node:sqlite встроен в Node, поэтому нативных зависимостей и сборки не потребовалось. Такой способ работает и для уже запущенного Claude, ничего в него не внедряя.
Окно, которое видно, но не мешает
Это вторая половина задачи. Нужно окно, которое:
-
прозрачное и без рамки;
-
лежит поверх всех окон, в том числе полноэкранных приложений;
-
не появляется в панели задач;
-
не забирает фокус (иначе во время набора текста ввод уйдёт в маскота);
-
пропускает клики на всё, кроме самого маскота.
В Electron большая часть этого — настройки BrowserWindow:
win = new BrowserWindow({ frame: false, transparent: true, backgroundColor: '#00000000', hasShadow: false, skipTaskbar: true, alwaysOnTop: true, focusable: false, /* ... */});win.setAlwaysOnTop(true, 'screen-saver');win.setIgnoreMouseEvents(true, { forward: true });
Самое интересное — последняя строка. setIgnoreMouseEvents(true, { forward: true }) делает окно «прозрачным» для кликов, но продолжает пересылать события движения мыши странице. Поэтому страница узнаёт, что курсор наведён на маскота (событие mouseenter), и тогда клики включаются обратно; когда курсор уходит, снова выключаются. Без этого пришлось бы вычислять форму окна вручную.
Окно растянуто на всю ширину рабочей области и имеет высоту 190 пикселей над панелью задач. Маскот просто «стоит» на панели задач, а сама анимация с топаньем и паром из головы — обычный SVG и CSS. Звук тоже ничего не весит: через WebAudio синтезируется «бормотание», а рот открывается по слогам. Чтобы звук играл без жеста пользователя, пришлось включить autoplayPolicy: ‘no-user-gesture-required’.
Что делает koffi
Чтобы понять, активно ли окно Claude, и вернуть его на передний план по клику, нужен WinAPI: GetForegroundWindow, поиск окна, SetForegroundWindow. Для этого я взял koffi: он вызывает функции из user32.dll без нативной сборки и без node-gyp. Правило «Claude активен» простое: процесс claude.exe или слово «claude» в заголовке окна. Если оно спрятано в трей, клик запускает claude.exe ещё раз, и приложение разворачивает существующее окно.
Активное окно опрашивается каждые 300 мс. Для таймера такой частоты нагрузка незаметна.
Уведомления Windows мешают сами себе
Если маскот показывается, а Windows параллельно рисует обычную плашку, получается два напоминания сразу, и это раздражает. Поэтому есть опция «Заменять плашку Claude маскотом», включённая по умолчанию. Она выключает баннеры только для Claude. Для этого в реестре ставится ShowBanner = 0 в HKCUSoftwareMicrosoftWindowsCurrentVersionNotificationsSettings. Записи в центре уведомлений при этом остаются, и база wpndatabase.db продолжает пополняться, так что маскот их по-прежнему видит.
Тут важно вернуть всё как было. Прежнее значение (или факт, что значения не было) сохраняется в конфиге и возвращается при выключении опции, при выходе, при завершении сеанса Windows и при следующем старте, если программа до этого упала. Прямо в реестр я пишу через reg.exe, так проще, чем тянуть ещё одну зависимость.
Поведение
Сценарий взят из небольшого HTML-демо, которое я нарисовал раньше самой программы, чтобы отладить характер:
-
Три выглядывания из угла, в каждом по три фразы, пауза между выглядываниями 7 секунд. В первых двух маскот спокоен, на третьем хмурится.
-
Потом он выходит целиком на 20 секунд: краснеет, ходит по низу экрана, топает, трясёт кулаками.
-
Уходит и повторяет всё заново, пока пользователь не откликнется.
-
Как только окно Claude стало активным или вы кликнули по маскоту, он убегает, цикл сбрасывается.
Фразы выбираются случайно, без повторов подряд.

Обновления
Программа собирается electron-builder в установщик NSIS и portable-файл. Установщик использует electron-updater с GitHub Releases: проверка через 15 секунд после запуска и потом раз в 6 часов, новая версия скачивается в фоне и ставится при перезапуске. Portable заменить себя не может (он запущен из временной распаковки), поэтому только сообщает о новой версии и открывает страницу выпуска.
Выпуск делается тегом v: GitHub Actions собирает и публикует релиз. Тут была своя ловушка: electron-builder загружает файлы параллельно и иногда создаёт дубли релиза, так что workflow сначала создаёт релиз командой gh release create, а потом уже отправляет файлы.
Автозапуск для portable тоже оказался с подвохом: process.execPath указывает на временную папку распаковки, поэтому путь берётся из переменной PORTABLE_EXECUTABLE_FILE и перерегистрируется при каждом старте.
Что в меню

Размер (1, 1.5, 2; по умолчанию 1.5), звук, автозапуск, язык (русский, английский или как в Windows), все переключатели сигналов, проверка обновлений. Для страховки есть пункт «Показать тест».
Установка
Ставить нужно либо установщик, либо один portable-файл. Оба на странице релиза:
-
https://github.com/OlegChe77/claude-mascot/releases/latest
-
исходники: https://github.com/OlegChe77/claude-mascot
Программа бесплатная, MIT. Если у вас есть идеи, какие ещё события Claude стоит ловить или какие фразы добавить, пишите в комментариях или создавайте Issue.
Источник: habr.com