DEV Community

s t
s t

Posted on

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 what is and is not covered.

No log scraping, no process polling

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 watcher

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:

  • Events that piled up while the app was closed are consumed on startup.
  • There is a 150 ms delay before reading, so a half-written file is less likely to be read. It is a mitigation, not a guarantee: the writer uses a plain writeFileSync, and a file that fails to parse is deleted anyway.
  • Each event file is deleted after it is handled. The app keeps a short in-memory list of recent tasks and a three-day per-agent log for a small "agent report" window.

Claude Code: a real hook

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.

Codex and Cursor: partial, and manual

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.

What the pet does about it

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.

The rest, briefly

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.

Get it

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.

Disclosure: I am the author of CatCode.

Top comments (1)

Collapse
 
autenai profile image
Auten •

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:

  • Your unused waiting state has a natural trigger in Claude Code: the Notification hook fires when Claude needs permission for a tool or has been idle waiting for input. Wiring it to a waiting event would let the pet tell "done" apart from "stuck on a permission prompt", which is the case people actually miss when they walk away.
  • Codex CLI has a notify setting in ~/.codex/config.toml that runs a program after each agent turn and passes it a JSON payload (type agent-turn-complete), so emit-event.js could read that instead of a manual call.
  • Recent Cursor versions ship agent hooks (hooks.json) with a stop event. Worth checking against the version your users run.
  • For the half-written file problem: write to <name>.tmp and then fs.renameSync to <name>.json. The watcher already ignores non-.json names, and a rename on the same volume shows up as a complete file, so the 150 ms delay stops being load-bearing.

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?