DEV Community

Cover image for I used Claude Code's hooks to pause a terminal game the moment my agent finishes
Kuldeep Mahour
Kuldeep Mahour

Posted on

I used Claude Code's hooks to pause a terminal game the moment my agent finishes

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 pauses 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.

Playing Snake until a banner says the AI is done

The idea: hooks plus one file

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
}
Enter fullscreen mode Exit fullscreen mode

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 pauses the game
  out.write('\x07');               // terminal bell
}
Enter fullscreen mode Exit fullscreen mode

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.

The bug a stranger found on Hacker News

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.

Not losing your game

Two small things make the pause feel right:

  • Focus reporting. The game turns on terminal focus events (\x1b[?1004h). When you click away, the terminal sends \x1b[O and the game pauses. When you come back, it sends \x1b[I.
  • A 3-2-1 countdown on every resume, so you don't crash into a wall the instant you return.

Keys typed in the first moment after a banner appears are ignored, because they were meant for the game, not the banner.

Opening it beside the agent

This was the fiddly part. /arcade tries, in order:

  • tmux: tmux display-popup -E -w 70% -h 70% agent-arcade, a floating window over your session
  • Ghostty: its quick terminal (if it's set to open centered) or a split, via AppleScript
  • iTerm2: a vertical split, via AppleScript
  • Anything else: a message telling you to open a new tab and run agent-arcade

Making it a one-line Claude Code plugin

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
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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.

Adding a game is one small file

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.

Try it

  • Claude Code: /plugin install agent-arcade --marketplace Kuldeep-045/agent-arcade, then /arcade
  • Any agent: 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?

Top comments (0)