Claude Code and Codex often take a few minutes on a task. In that time I either stare at the spinner, or switch to another tab and forget to come back for ten minutes.
So I built Agent Arcade: tiny terminal games to play during the wait. The useful part isn't the game. It's that the game s and tells you the moment your agent is done or needs you, so the break never runs long.
This post is about how that works, because the same trick (an agent hook that writes a file, something else that watches it) is handy for a lot more than games.
Claude Code can run a command at certain moments of a session. Three of those moments matter here:
| Hook | Fires when | Agent Arcade says |
|---|---|---|
Stop |
Claude finishes its turn | "done" |
PermissionRequest |
Claude shows a permission prompt | "ask" |
Notification |
other "waiting for you" moments, like an idle prompt | "ask" |
Each hook runs one tiny command, agent-arcade notify or agent-arcade notify ask. All it does is write a word to a file:
// bin/agent-arcade.mjs
else if (cmd === 'notify') {
fs.mkdirSync(DIR, { recursive: true });
fs.writeFileSync(SIGNAL, arg === 'ask' ? 'ask\n' : 'done\n'); // ~/.agent-arcade/signal
}
No server, no socket, no daemon. The game, running in another terminal, checks that file's modified time on every tick. A new time means a new event:
// src/shell.mjs
const stamp = () => { try { return fs.statSync(SIGNAL).mtimeMs; } catch { return 0; } };
let seen = stamp();
function checkAgent() {
const t = stamp();
if (t === seen) return; // nothing new
seen = t;
let kind = 'done';
try { kind = fs.readFileSync(SIGNAL, 'utf8').trim() === 'ask' ? 'ask' : 'done'; } catch {}
agent = kind; // shows the banner and s the game
out.write('\x07'); // terminal bell
}
Using the file's modified time instead of its contents means two "done" events in a row still count as two events, and a game started after an event doesn't react to an old one.
The same file works for Codex, whose notify setting runs a program after each turn. Any agent that can run a command when it finishes can use it.
My first version used Notification for "Claude needs you". On my Show HN post, a commenter pointed out something I'd missed in the docs: the permission-prompt notification only fires after you haven't typed for about six seconds. Mid-game, you're pressing keys constantly, so the "Claude is waiting" banner could arrive late.
PermissionRequest fires as soon as the prompt appears. The fix was one line in the setup, and the plugin now listens to it directly. The hook never answers the prompt: it only writes "ask", and you approve or deny as usual.
If you write hooks for "the agent needs me" alerts, use PermissionRequest, not Notification.
Two small things make the feel right:
\x1b[?1004h). When you click away, the terminal sends \x1b[O and the game s. When you come back, it sends \x1b[I.
Keys typed in the first moment after a banner appears are ignored, because they were meant for the game, not the banner.
This was the fiddly part. /arcade tries, in order:
tmux display-popup -E -w 70% -h 70% agent-arcade, a floating window over your session agent-arcade
At first, setup was npm install -g agent-arcade and then agent-arcade setup, which edits ~/.claude/settings.json. That's two steps and a settings file edit.
Now it's a Claude Code plugin, installed from the GitHub repo, which doubles as a plugin marketplace:
/plugin install agent-arcade --marketplace Kuldeep-045/agent-arcade
The plugin registers /arcade as an immediate command, so it runs without a model turn. It opens the game instantly, even while Claude is in the middle of a task, and doesn't add anything to the conversation. It also listens to the same hook events, so there's no settings file to edit:
on('classic.Stop', async ($, e, next) => {
arcade($, ['notify'], 5000).catch(() => {}) // fire and forget
return next(e) // never change what Claude does
})
on('classic.PermissionRequest', async ($, e, next) => {
arcade($, ['notify', 'ask'], 5000).catch(() => {})
return next(e)
})
The repo only needs two extra files to be installable: the plugin's manifest and a .claude-plugin/marketplace.json listing it. If agent-arcade isn't installed globally, the plugin falls back to npx.
Each game is a plain object: start, tick, key, draw, score. The shell handles the screen, input, pausing, banners and best scores. Snake is about 60 lines. If you have a favourite game for a two-minute wait, I'd love a pull request.
/plugin install agent-arcade --marketplace Kuldeep-045/agent-arcade, then /arcade
npm install -g agent-arcade, then agent-arcade setup
Code (MIT, no dependencies, no tracking): https://github.com/Kuldeep-045/agent-arcade
What game would you want next?