I often give Claude Code a long task and switch to another window. Ten minutes later I find out it has been stuck on a "can I run this command?" prompt the whole time. The Windows toast did show up, for about five seconds, while I was looking elsewhere.
I didn't want a timer, I wanted something I can't miss. So I built Mascot: a small orange guy who peeks out of the bottom-right corner of the screen when Claude is waiting for you. If you ignore him, he comes out, turns red and stomps along the taskbar.
It's free, open source (MIT), Electron plus a bit of WinAPI through koffi. Here's how it works.
How it knows Claude is waiting
Claude Code hooks. You can configure a command in ~/.claude/settings.json that runs on an event and gets JSON on stdin. The app starts a tiny HTTP server on 127.0.0.1:47821 and the hook just pipes stdin to it:
{
"hooks": {
"Notification": [
{ "hooks": [ { "type": "command",
"command": "curl -s -m 2 --data-binary @- http://127.0.0.1:47821/hook || exit 0" } ] }
]
}
}
|| exit 0 makes it silently do nothing when the app isn't running, and the same line works in bash and cmd. The server rejects requests that carry an Origin header, so a random web page can't poke it.
The app installs the hooks itself at startup and keeps backups (settings.json.mascot-*.bak, the last five).
The desktop app doesn't send Notification. When Claude desktop (the Code tab) asks for a permission or asks a question, it does not fire Notification. I only found this out because the terminal version worked and the desktop one didn't. So the app also hooks PermissionRequest and PreToolUse with the matcher AskUserQuestion|ExitPlanMode. Stop is optional, if you want a nudge after every reply.
Windows notification database. Windows writes every toast into %LOCALAPPDATA%\Microsoft\Windows\Notifications\wpndatabase.db. Polling it once a second and looking for the Claude_*!Claude app id works even for an already running Claude, and node:sqlite is built into Node, so there are no native modules:
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();
A window that is visible but doesn't get in the way
It has to be transparent, frameless, always on top, absent from the taskbar, unable to take focus, and click-through everywhere except the mascot:
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 });
The trick is forward: true. The window ignores clicks, but mouse-move events still reach the page. When the cursor is over the mascot (mouseenter), the page tells the main process to turn clicks back on, and off again when it leaves. No manual hit-testing.
The window is as wide as the work area and 190 px tall, sitting right above the taskbar. The animation is SVG and CSS. The grumbling is synthesized with WebAudio, and the mouth opens on syllables. For sound without a user gesture you need autoplayPolicy: 'no-user-gesture-required'.
To know whether Claude is active and to bring its window to the front on click, I call user32.dll through koffi, with no node-gyp. "Claude is active" means a claude.exe process or "claude" in the window title. Polling the foreground window every 300 ms is cheap.
Don't show two reminders
If Windows draws its own toast at the same time, you get two reminders. The option "Replace Claude pop-ups with the mascot" (on by default) turns off Claude's banners only: it sets ShowBanner = 0 under HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\<AUMID> (via reg.exe). The Action Center entries and the database rows remain, so the mascot still sees them.
The important part is restoring the old state. The previous value (or the fact that there was none) is stored in the config and restored when you turn the option off, on quit, on Windows session end, and on the next start after a crash.
Behaviour
- Three peeks from the corner, three phrases each, 7 seconds between them. Calm in the first two, grumpy in the third.
- Then he comes out completely for 20 seconds: red, stomping along the bottom, shaking fists.
- Leaves and repeats until you respond.
- If the Claude window becomes active or you click the mascot, he runs away and the cycle resets.
Phrases are random, never the same twice in a row. I drew an HTML demo of this before writing any of the app, to tune the character.
Updates and releases
Built with electron-builder into an NSIS installer and a portable exe. The installer uses electron-updater with GitHub Releases: check 15 seconds after start, then every 6 hours, download in the background, install on restart. A portable exe can't replace itself (it runs from a temp extraction), so it only tells you a new version exists.
Two traps along the way: electron-builder uploads files in parallel and can create duplicate releases, so the workflow creates the release with gh release create first. And autostart for portable must use PORTABLE_EXECUTABLE_FILE, not process.execPath, which points at the temp folder.
Get it
- Download (installer, self-updating, or a portable exe): https://github.com/OlegChe77/claude-mascot/releases/latest
- Source: https://github.com/OlegChe77/claude-mascot
Ideas for more events to catch or phrases to add are welcome in the comments or as issues.




Top comments (0)