{"slug": "how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task", "title": "How a desktop pet finds out your AI agent has finished a task", "summary": "A developer built CatCode, an Electron-based pixel desktop pet for Windows that reacts when a coding agent finishes a task, using a simple drop-folder protocol at ~/.catcode/events/ where any tool can write a small JSON file with source, task and message fields. The app consumes those files via fs.watch and triggers a celebration animation, with a tray item that writes a Claude Code Stop hook into ~/.claude/settings.json; the developer notes there is no Codex- or Cursor-specific integration, so those tools work only if a completion command is wired manually. The pet is drawn procedurally on a 28x28 offscreen canvas rather than from a sprite sheet.", "body_md": "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.\n\nCatCode 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**:\n\n```\n~/.catcode/events/\n```\n\nAnything that can write a small JSON file there can tell the pet a task is done:\n\n```\n{ \"source\": \"Codex\", \"task\": \"update the UI\", \"message\": \"finished the check\" }\n```\n\nThat is the entire protocol: `source`, `task` and `message`. Missing values fall back to defaults.\n\nThe main process consumes those files with `fs.watch`. Trimmed from `main.js`:\n\n``` js\nfunction consumeEvent(file) {\n  const full = path.join(EVENTS_DIR, file), data = loadJSON(full, null);\n  try { fs.unlinkSync(full); } catch {}\n  if (!data) return;\n  // ...append to a per-day log (only the last 3 days are kept)\n  celebrate(`Task complete\\n${src} ${msg}`);\n}\nfunction watchEvents() {\n  for (const f of fs.readdirSync(EVENTS_DIR)) if (f.endsWith('.json')) consumeEvent(f);\n  fs.watch(EVENTS_DIR, (_e, file) => { if (file && file.endsWith('.json'))\n    setTimeout(() => { if (fs.existsSync(path.join(EVENTS_DIR, file))) consumeEvent(file); }, 150); });\n}\n```\n\nDetails that come straight from this code:\n\n`writeFileSync`, and a file that fails to parse is deleted anyway.\nFor 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):\n\n```\n{\n  \"hooks\": {\n    \"Stop\": [\n      { \"hooks\": [\n        { \"type\": \"command\",\n          \"command\": \"node C:\\\\catcode\\\\tools\\\\emit-event.js \\\"Claude Code\\\" \\\"\\\" \\\"finished the task\\\"\" }\n      ] }\n    ]\n  }\n}\n```\n\n`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.\n\nTwo 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.\n\nThis 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.\n\nWhat exists is the generic path: if your tool has a \"run a command when finished\" mechanism, call the same script with a different source:\n\n```\nnode tools/emit-event.js \"Codex\" \"update the UI\" \"finished the check\"\n```\n\nIf 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.\n\n`celebrate()` sends the `happy` state to the renderer, shows a speech bubble for 6 seconds, then restores whatever state the pet was in before.\n\nThere 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.\n\nThe 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.\n\nSkins are small JSON files rather than images:\n\n```\n{ \"id\": \"ginger-cat\", \"species\": \"cat\", \"pattern\": \"tabby\",\n  \"palette\": { \"body\": \"#f2954e\", \"shade\": \"#c26a2c\", \"belly\": \"#fbe0c2\",\n               \"stripe\": \"#b8531f\", \"eye\": \"#4fae5a\", \"nose\": \"#d98c9a\", \"inner\": \"#f0a9b3\" } }\n```\n\nSpecies, pattern (`solid`, `tabby`, `patch`), optional accessory and a palette drive the same drawing code, so a new colour scheme needs no art.\n\nIt 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.\n\nCatCode is a Windows app, and the source is closed (proprietary EULA). It costs $5: [CatCode on Gumroad](https://chvdbox.gumroad.com/l/tlwwyz?utm_source=devto&utm_medium=article&utm_campaign=catcode).\n\nThe installer is not code-signed, so Windows SmartScreen will show an \"unknown publisher\" warning on first launch.\n\nDisclosure: I am the author of CatCode.", "url": "https://wpnews.pro/news/how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task", "canonical_source": "https://dev.to/s_t_ae6f13c69a431fda2ee4b/how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task-7ca", "published_at": "2026-10-18 22:11:31+00:00", "updated_at": "2026-10-18 22:19:02.722475+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["CatCode", "Claude Code", "Codex", "Cursor", "Electron", "Node.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task", "markdown": "https://wpnews.pro/news/how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task.md", "text": "https://wpnews.pro/news/how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task.txt", "jsonld": "https://wpnews.pro/news/how-a-desktop-pet-finds-out-your-ai-agent-has-finished-a-task.jsonld"}}