Technical · AI
We used a Claude Code mod to build a notification system for AI Agents.
October 2, 2026
A Claude Code mod is a plugin that runs inside the session. Most examples show a status line or a button. We used one to build a notification system.
An AI agent sits idle at its prompt. Another agent sends it a message. The message lands in a mailbox on disk, and nothing tells the agent it has mail.
That is the gap between “delivered” and “told.” Hooks cannot close it because a hook runs only when Claude Code is already doing something. A server cannot close it either when that server didn't start the session, and nobody owns its terminal.
A mod can close it, because it lives inside the session. This is how we built one, and what each piece of the mod API does in it.
What a mod is
Mods are on by default from Claude Code 2.1.287 (before that, they were in early access). A mod is a plugin with a hooks/hooks.json that points to a module:
{ "modules": ["./register.tsx"] }The module registers hooks on events: a tool call, a submitted prompt, a turn starting or ending, the interface being drawn. Every hook receives $, the engine interface and an event. It can answer the event, change it, or pass it along.
What makes mods different from hooks is that a mod keeps running between events. It can hold a timer, keep state, draw into the interface, and start a turn when the session is idle.
The idea
Give the agent’s own session a small client that watches the agent’s own mailbox.
Every 20 seconds, the mod counts unread messages. If the number went up, the person sees it inside Claude Code without opening any dashboard. If they opted in, the session wakes itself and reads the mail.
It needs no server push, no terminal access, and no public URL. The session finds out about its own mail, which makes this the only option that works on a machine we do not run.
The mod: about 100 lines, six parts of the API
We called it amp-inbox-band. Here is what it uses.
- $.clock.every runs the poll every 20 seconds.
- $.process.run runs one script that counts unread messages in the mailbox.
- $.ui.status and $.ui.toast show ✉ 3 unread in the status line and pops a toast when the count rises.
- A ui.render hook on AbovePrompt draws a band above the prompt with a Read button, using the elements the surface provides (Box, Text, Button).
- $.command.register adds /amp-inbox, which lists mail without spending a model turn.
- $.prompt.submit wakes an idle session. This is the part hooks cannot do.
State (the unread count, whether the band is hidden) lives in $.state atoms, so a write redraws whatever reads it.
The core of the poll, trimmed:
async function poll($: Engine, autoWake: boolean) {
const n = await countUnread($)
if (n === null) return
await update($, unread, () => n)
$.ui.status(n > 0 ? `✉ ${n} unread` : undefined)
if (n > lastSeen) {
await update($, isHidden, () => false)
$.ui.toast(`${n} unread AMP message${n === 1 ? '' : 's'}`)
if (autoWake) {
if (isBusy) isWakePending = true
else await wake($)
}
}
lastSeen = n
}The isBusy flag comes from two more hooks, turn.start and turn.complete. If mail arrives while the agent is mid-turn, the mod does not interrupt. It waits for the turn to end, counts again, and wakes only if something is still unread. A message the agent already read does not trigger anything.
Two design choices worth copying
The wake prompt never contains the message. It says “you have unread messages, read your inbox” and nothing more. Message bodies are text from another party, and if a mod pasted one into a prompt, it would hand a stranger the user’s voice. The agent reads the mail through the normal path, where it is wrapped as data.
Waking is also off by default. It is an userConfig option, autoWakeand it stays false until the person turns it on. A mod that can start turns on its own should ask first, and ours adds a five-minute cooldown.
How we built and tested it
The tooling covers the whole loop. Write the mod into a dev folder, and Claude Code asks once whether to enable hot reloading for the session. After that, each edit reloads the mod when the turn ends, so we iterated on the live mod with the real session open.
claude plugin validate reads the module and lists every call it makes before you run it. Ours runs one script, reads one environment variable, submits a prompt, and draws, with no network and no files. That matters because mods are not sandboxed.
claude plugin test runs *.test.ts files against a mocked engine with a mock clock. We wrote four tests, including one that checks a message arriving mid-turn and read before the turn ends does not wake the session. That test fails if you remove the deferral.
What it did
We tested between a Mac with no tmux (running the mod) and a Linux host, over the AMP mesh.
- A message arrives, and the person sees it inside Claude Code: pass.
- A message arrives while the session is idle: it woke 9 seconds later.
- The same across hosts: 17 seconds, inside the 20-second poll.
- A message arrives mid-turn and is read before the turn ends: no wake afterwards.
- A reply from the other host wakes the idle session: pass.
Latency is the poll interval and nothing else. These are single measurements, not a benchmark.
Limits
- The API is early access and moves between releases.
- Mods draw only in the terminal and the Desktop app. In the VS Code chat panel and under claude -p the hooks run, but nothing shows.
- Admins can block user-installed mods.
- Mods run with your permissions. Run claude plugin validate on any mod before you trust it.
- Codex and other runtimes have no mods, so this is a Claude Code answer, not a universal one.
Other things this shape fits
Nothing here is specific to messages. A mod that polls a source, compares to what it saw last, shows the result in the interface, and optionally starts a turn would also fit a CI status, a deploy queue, a ticket assigned to the agent, or a build that finished. The pattern is the same: count, compare, show, ask before waking.
Try it
You need Claude Code 2.1.287 or later.
git clone https://github.com/23blocks-OS/ai-maestro
claude --plugin-dir ai-maestro/mods/amp-inbox-band
Validate it first and read what it does. The source is in mods/amp-inbox-band/, and the versions, limits, and test results are in docs/CLAUDE-CODE-MODS.md. Waking is off until you enable autoWake in the plugin config.
AI Maestro is open source (MIT): https://github.com/23blocks-OS/ai-maestro
Independent project. Claude and Claude Code are Anthropic’s products; this article is not affiliated with or endorsed by Anthropic.
Originally published on Medium in AI Maestro.
Related reading
- Make your AI agents remember better with a Karpathy wiki
October 26, 2026 · AI
- Real Technology, Real Mission: An AI Integration for Intercambio Uniting Communities
October 24, 2026 · AI
- Running AI Locally: What NotoNote Keeps on Your Machine
October 24, 2026 · AI
More in the Digest — a weekly roundup of AI and agents news — or start with AI Strategy.