Hotaru – Single dot in your macOS menu bar showing CLI coding agents status Hotaru, a new macOS menu bar app from developer Ajwad Javed, displays a single dot that turns red when CLI coding agents are thinking and green when they are free, supporting Cursor CLI and Claude Code. The app uses hook systems from both CLIs to write JSON state files rendered by a SwiftBar plugin, avoiding screen-scraping or polling, and is designed to never block or alter agent operations. It is available via GitHub and requires macOS Monterey or later, SwiftBar, and Python 3. A single dot in your macOS menu bar that tells you whether your CLI coding agents are still working or waiting on you. Red means thinking. Green means free. Install install · The name the-name · How it works how-it-works · Things worth knowing things-worth-knowing If you drive coding agents from the terminal, you end up with several sessions running at once across tabs, splits and tmux windows. There's no way to tell which of them is grinding away and which finished four minutes ago and is quietly waiting for you. So you cycle through panes to check, which is exactly the kind of interruption the agents were meant to remove. Hotaru puts that answer in the corner of your screen. One glance, no pane-hopping. The dot is the whole interface. Open it and you get a line per session with its state, how long it has been in that state, its project path and the model it's running. | Dot | Meaning | |---|---| | Grey, hollow | No sessions running | | Red | At least one agent is thinking | | Green | Every session is free and waiting on you | The badge disambiguates when you have several sessions. 1/3 means one of three is thinking; a bare 3 means all three are in the same state; a single session shows no number at all. Hotaru 蛍 is Japanese for firefly . It fits on three levels. A firefly is a small glowing dot that signals by light alone, which is precisely the interface here. A swarm of them maps onto a set of sessions, each blinking its own state. And fireflies are the classic image of a summer night in Japan, which suits the Kanagawa https://github.com/rebelot/kanagawa.nvim palette the whole thing is coloured with — the same greens and reds you'd use in a Neovim or terminal theme, so it sits with the rest of your setup instead of shouting over it. No screen-scraping and no polling of the agents. Both CLIs expose a hook system, so Hotaru asks them directly. cursor-agent ─┐ ├─→ ~/.hotaru/report.py ─→ ~/.hotaru/sessions/ .json ─→ SwiftBar plugin ─→ ● Claude Code ──┘ report.py is registered as a hook on a handful of lifecycle events. On each one it writes a small JSON file describing that session. Submitting a prompt or starting a tool call marks it busy; the stop event marks it free; the session-end event deletes the file. | Event | cursor-agent | Claude Code | Result | |---|---|---|---| | Session opens | sessionStart | SessionStart | green | | You submit a prompt | beforeSubmitPrompt | UserPromptSubmit | red | | A tool call begins | preToolUse | PreToolUse | red | | The turn finishes | stop | Stop | green | | Session closes | sessionEnd | SessionEnd | removed | The plugin renders those files. Because hooks fire on state changes, report.py also pokes SwiftBar's URL scheme so the dot flips immediately rather than waiting for the next poll. The refresh interval in the plugin filename exists only to clean up after sessions that died without a chance to say goodbye. Two properties worth calling out, since this code runs inside your agent's critical path: It cannot block or alter an agent. report.py swallows every exception and always exits 0 with empty stdout. A bug in it makes the dot wrong, not your session broken. It recovers from crashes. Each state file records the agent's real process ID. If a session is SIGKILL ed or its terminal is closed, the plugin notices the process is gone and drops it, so you never get a dot stuck on red. A 30-minute stall timeout backs that up. - macOS Monterey or later, as SwiftBar requires SwiftBar https://swiftbar.app — install.sh will install it via Homebrew if missing python3 - At least one of Cursor CLI https://cursor.com/cli or Claude Code https://claude.com/product/claude-code git clone https://github.com/ajwadjaved/Hotaru.git cd Hotaru ./install.sh The installer copies the reporter to ~/.hotaru/ , drops the plugin into your SwiftBar plugin folder, merges the hook entries into your CLI configs and starts SwiftBar. It's safe to re-run. Your existing config is merged, not replaced — ~/.claude/settings.json in particular holds your model choice, status line and other settings. Every file is backed up before it's touched, and re-parsed before being saved. After installing, start a new agent session.Both CLIs read their hook config only at startup, so sessions you already have open won't appear. This is the single most common surprise. Manual install mkdir -p ~/.hotaru/sessions install -m 755 src/report.py ~/.hotaru/report.py install -m 755 src/hotaru.5s.py ~/.swiftbar/hotaru.5s.py or your SwiftBar plugin folder Then add the hook entries from examples/ /ajwadjaved/Hotaru/blob/main/examples to ~/.cursor/hooks.json and ~/.claude/settings.json . Use absolute paths — those commands are not shell-expanded, so ~ and $HOME won't resolve. Or just run the merge step on its own: python3 src/wire hooks.py Everything is two short Python files; edit them in place. Colours are the constants at the top of hotaru.5s.py . They're Kanagawa Wave https://github.com/rebelot/kanagawa.nvim by default, so they match a typical Neovim or starship setup. SAMURAI RED is quite hot in a menu bar — C34043 is a calmer alternative. Refresh interval is the filename. Rename to hotaru.10s.py to halve the background work; state changes still appear instantly via the URL-scheme nudge. Dropdown contents are the main function. After editing the plugin, refresh it: open -g "swiftbar://refreshplugin?name=hotaru" Four non-obvious behaviours, all of which cost real debugging time and are baked into the code now. Hooks load at startup. Neither CLI re-reads its hook config mid-session, so changes only take effect in new sessions. -p print mode fires no hooks. Running cursor-agent -p "..." non-interactively won't register a session. Only interactive sessions report, which is fine for the intended use but makes scripted testing misleading. To test, drive a real session — a tmux pane with send-keys works well. A hook's parent process is not the agent. Hooks are spawned through a short-lived shell that exits immediately, so getppid is useless for liveness. report.py walks up the process tree to find the long-lived agent process, stopping when it hits the terminal emulator. Without this, sessions get reaped seconds after they appear. SwiftBar renders sfimage as a template image. That forces it monochrome, so sfcolor is silently ignored and you get a white blob. Hotaru draws a ● text character coloured with color= instead, which can't be overridden. sfcolor only applies to SF Symbols embedded in the title text via :symbol: syntax. The dot stays hollow grey. Confirm a session actually registered: ls ~/.hotaru/sessions/ one JSON file per live session cat ~/.hotaru/events.log every hook fired, with the resolved agent pid An empty events log means the hooks aren't wired or the session predates them. Start a fresh session and check again. The dot is there but never changes colour. Run the plugin by hand — its output tells you what SwiftBar is being asked to draw: ~/.swiftbar/hotaru.5s.py A session lingers after closing. Only possible if the agent process is somehow still alive; the reaper is keyed on that. rm ~/.hotaru/sessions/ .json clears the slate safely. ./uninstall.sh Removes the hook entries, the plugin and ~/.hotaru , leaving SwiftBar installed and your config backups in place. A distinct colour for "waiting for your approval", which is currently lumped in with red because red means "not free". Claude Code's Notification event gives this exactly; cursor-agent has no equivalent event, but a session blocked on a TTY read sits at 0% CPU, which is a reliable enough signal. Clicking a session to jump to its terminal is also within reach — Ghostty's AppleScript dictionary can focus a tab, and tmux can select a window. MIT — see LICENSE /ajwadjaved/Hotaru/blob/main/LICENSE .