DEV Community

Cover image for How I used Claude Code hooks to build a menu bar watcher for all my AI coding agents
Amit Raz
Amit Raz

Posted on

How I used Claude Code hooks to build a menu bar watcher for all my AI coding agents

I usually have three or four AI coding agents running at the same time. Claude Code in one terminal working through a refactor, Codex in another, Cursor open on a second feature, sometimes Copilot in the background. The actual coding part is great. The part that wore me down was the bookkeeping.

Because the thing about running agents in parallel is that they don't finish in sync. One stops to ask whether it can run a migration. Another quietly waits on a permission prompt. A third is still grinding. And there's no single place to see any of that, so you end up cycling through windows every couple of minutes just to check who needs you. I'd get deep into one terminal and a "can I edit this file?" would sit untouched in another for ten minutes. I wasn't coding at that point. I was babysitting.

So I built a small thing to fix it. This is how it works and the one design decision that made it actually useful.

Why I didn't just use an existing tool

There are tools that aggregate multiple agents. I tried a few. They mostly work by wrapping your agents: you launch and run everything through their UI, and in exchange you get a combined view.

That was a non-starter for me. I've spent years getting my terminal, my editor, and my multiplexer set up the way I like. I don't want to move my whole workflow inside someone else's shell just to get a status light. I wanted something that watched from the outside and changed nothing about how I work.

That ruled out wrapping. Which left the question: how do you know what an agent is doing without sitting between the user and the agent?

The insight: agents already announce their state

You don't have to intercept anything, because the agents already tell you when something happens. You just have to listen.

Claude Code has a hooks system. Hooks are shell commands that Claude Code runs automatically at specific points in its lifecycle, and they're defined in a JSON settings file. Two of them are exactly what I needed:

  • Stop fires when Claude Code finishes responding, in other words when a turn ends.
  • Notification fires when Claude Code needs your attention, like when it's waiting on input or a permission prompt.

The key property is that hooks are deterministic. They don't depend on the model deciding to tell you something. When the event happens, the command runs, every time. That's the difference between a reliable status light and one that's right most of the time.

When a hook runs, Claude Code passes it JSON on stdin with context about the event, including things like the session and the working directory. So a hook handler can read that, figure out what just happened, and record it somewhere.

The architecture: write small files, watch a folder

Here's the decision that made the whole thing simple. Instead of having the app talk to each agent, I had each agent's hook write a tiny status file, and I pointed the app at the folder those files live in.

A Stop or Notification hook just writes a small JSON file for that session. Roughly:

{
  "session": "billing-refactor",
  "tool": "claude-code",
  "state": "waiting",
  "cwd": "/Users/me/code/billing",
  "updated_at": "2026-06-30T09:14:22Z"
}
Enter fullscreen mode Exit fullscreen mode

The hook config that produces it is ordinary Claude Code settings. A simplified version of the idea:

{
  "hooks": {
    "Notification": [
      { "hooks": [{ "type": "command", "command": "theeye-status waiting" }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "theeye-status idle" }] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The app does one job: it watches that folder and folds every status file into a single picture. No sockets, no daemon to babysit, no agent talking to another agent. A file appears or changes, the app re-reads the folder, the icon updates.

This decoupling is what makes it pleasant to live with. The agents don't know the app exists. The app doesn't know how the files got there. And adding a new agent later means writing one more hook that drops a file in the same folder. The viewer doesn't change at all.

It's also why the privacy story is short. The status files carry the session name, the tool, and the state. They never carry your code, your prompts, or the model's output, because the watcher never needs any of that to tell you who's waiting. Everything stays on your machine. There's no account and no server.

The part you actually look at

The viewer is a native macOS menu bar app, built with SwiftUI's MenuBarExtra. It runs as a background agent with no Dock icon, because a thing that watches all day shouldn't take up a Dock slot.

There's one eye in the menu bar, and it changes by state. It opens red with a count when a session is waiting on you. It watches calmly while everything's working. It closes when it's all quiet. I made the shape change along with the color, not just the color, so it's still readable if you don't catch the exact hue out of the corner of your eye.

Two things turned it from "neat" into "I use this every day." First, clicking a waiting session brings that exact terminal or editor window to the front, whether that's tmux, iTerm, WezTerm, Cursor, or VS Code. No hunting for the right window. Second, the alerts are opt-in and layered: a sound, a native notification, or having the waiting window pull itself forward on its own. You pick how loud it is.

Going from one agent to many

Once the file-watching pattern was in place, supporting more agents was almost boring, which is the point. Each one has its own lifecycle hooks or events, and each one gets a small handler that writes the same kind of status file into the same folder. Today it watches Claude Code, Codex CLI, Cursor (CLI and in-editor), Copilot (CLI and in VS Code), and Google Antigravity. They all collapse into the one eye.

Reaching past the Mac

The last gap was physical. The whole point is to stop watching windows, but a menu bar icon still assumes you're at the screen. So I added Telegram. When a session starts waiting, it can ping your phone, which means you can walk away and still know the moment something needs you.

The design followed the same restraint as the rest. You connect a bot from Settings in about a minute. The bot token lives in the macOS Keychain, not in a plist or a config file. The message carries only the agent and the project name. And nothing is sent until you turn it on.

What I'd tell you if you're building something similar

The lesson that generalized best: when you want to observe a system, look for the events it already emits before you reach for interception. Hooks meant I never had to wrap, proxy, or parse a terminal. And writing plain files to a folder, instead of wiring components directly to each other, kept every piece ignorant of the others, which is exactly what you want when you'll be adding more pieces later.

If you run more than one coding agent at a time and you recognize the window-cycling, the app is free, notarized, and runs on macOS 14 or later. It's at theeye.dev.


I'm Amit Raz, a software architect and AI consultant. I build small tools like this under RZApps and help teams put AI to work in their own products and workflows. More of what I'm building and writing is at rzailabs.com.

Top comments (0)