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.

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.

Latency is the poll interval and nothing else. These are single measurements, not a benchmark.

Limits

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

More in the Digest — a weekly roundup of AI and agents news — or start with AI Strategy.