How a desktop pet finds out your AI agent has finished a task
CatCode is a pixel pet that lives on the Windows desktop and reacts when a coding agent finishes a task. This is a walkthrough of how that detection works, taken from the app's code (an Electron app). It is simpler than you might expect, and support differs by agent, so I will be specific about wha

CatCode is a pixel pet that lives on the Windows desktop and reacts when a coding agent finishes a task. This is a walkthrough of how that detection works, taken from the app's code (an Electron app). It is simpler than you might expect, and support differs by agent, so I will be specific about what is and is not covered.
CatCode does not tail agent logs, inspect processes or read any agent's internal files. I checked: there is no such code. The whole mechanism is a drop folder:
~/.catcode/events/ Enter fullscreen mode Exit fullscreen mode Anything that can write a small JSON file there can tell the pet a task is done:
{ "source": "Codex", "task": "update the UI", "message": "finished the check" } Enter fullscreen mode Exit fullscreen mode That is the entire protocol: source, task and message. Missing values fall back to defaults.
The main process consumes those files with fs.watch. Trimmed from main.js:
function consumeEvent ( file ) { const full = path. join ( EVENTS_DIR, file ), data = loadJSON ( full, null ); try { fs. unlinkSync ( full ); } catch {} if (! data ) return; //...append to a per-day log (only the last 3 days are kept) celebrate ( `Task complete\n ${ src } ${ msg } ` ); } function watchEvents () { for ( const f of fs. readdirSync ( EVENTS_DIR )) if ( f. endsWith ( '.json ' )) consumeEvent ( f ); fs. watch ( EVENTS_DIR, ( _e, file ) => { if ( file && file. endsWith ( '.json ' )) setTimeout (() => { if ( fs. existsSync ( path. join ( EVENTS_DIR, file ))) consumeEvent ( file ); }, 150 ); }); } Enter fullscreen mode Exit fullscreen mode Details that come straight from this code:
For Claude Code the trigger is its Stop hook. The app has a tray item that writes the hook into ~/.claude/settings.json for you. The resulting entry has this shape (the path to the script is yours to set):
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "node C: \\ catcode \\ tools \\ emit-event.js \" Claude Code \" \"\" \" finished the task \" " } ] } ] } } Enter fullscreen mode Exit fullscreen mode emit-event.js is about 30 lines: it creates the folder if needed and writes {source, task, message} as a timestamped.json file. It also accepts JSON on stdin and uses session_id as the task, if you call it without arguments.
Two honest caveats. First, Stop fires when Claude Code finishes responding, so "task finished" really means "the agent stopped and handed control back". Second, the hook command starts with node, so Node.js needs to be on your PATH.
This is the part to read carefully. The code has no Codex-specific or Cursor-specific integration. No config file for either is written, and nothing watches either tool. The names appear in the README, in the description, and in the tray's "test" button, which just plays a canned message.
What exists is the generic path: if your tool has a "run a command when finished" mechanism, call the same script with a different source:
node tools/emit-event.js "Codex" "update the UI" "finished the check" Enter fullscreen mode Exit fullscreen mode If it has no such hook, you can run that line by hand. The pet reacts identically. So: Claude Code has a tray item that writes the hook for you (or you add it by hand, as above), Codex and Cursor work only if you wire a completion command yourself, and I can't tell you from this code whether either tool offers a suitable hook.
celebrate() sends the happy state to the renderer, shows a speech bubble for 6 seconds, then restores whatever state the pet was in before.
There is no sprite sheet. The pet is drawn procedurally: Sprite.makeRenderer(28) creates a 28x28 offscreen canvas, and primitives such as pdisc, ptri and pline plot pixels, which are then scaled up (110, 140 or 180 px depending on the size setting). A pose() function maps the state to parameters. For happy, the code makes the pet bounce, smile, stick out its tongue and wag its tail faster, while the renderer spawns sparks and hearts.
The full state list is: idle, working, waiting, sleeping, happy, play, run, pull. Note that working (the laptop pose) is set by the Pomodoro focus timer or by hand from the tray, not by agent activity. The pet never knows a task is in progress, only that one ended.
Skins are small JSON files rather than images:
{ "id": "ginger-cat", "species": "cat", "pattern": "tabby", "palette": { "body": "#f2954e", "shade": "#c26a2c", "belly": "#fbe0c2", "stripe": "#b8531f", "eye": "#4fae5a", "nose": "#d98c9a", "inner": "#f0a9b3" } } Enter fullscreen mode Exit fullscreen mode Species, pattern ( solid, tabby, patch ), optional accessory and a palette drive the same drawing code, so a new colour scheme needs no art.
It also does the desktop-pet things: eyes and head follow the cursor, Pomodoro focus and break timers, water and stretch reminders, user reminders, and skin import. Memes and images from the internet are off by default and need to be enabled in the tray. The privacy note lists the services the app contacts directly: ipapi.co and open-meteo.com for weather, plus meme/image services only if you opt in.
CatCode is a Windows app, and the source is closed (proprietary EULA). It costs $5: CatCode on Gumroad.
The installer is not code-signed, so Windows SmartScreen will show an "unknown publisher" warning on first launch.
Expand Auten Auten Auten Follow Auten gives your AI agent hands: it clicks, types and reads a real screen on your computer (Mac/Windows/Linux) and Android phone (beta). Works with any MCP client. Joined Oct 7, 2026 • Oct 18 Dropdown menu Copy link Hide Nice honest write-up, especially the part about what the code does NOT do for Codex and Cursor. A few hooks that could fill the gaps:
One question: as you note, Stop means "handed control back", which includes a turn that ends with a question for you or gave up halfway. Do you plan to look at the hook's stdin (or the transcript path in it) to tell a real finish apart, or is any stop worth a celebration?