ajwadjaved/hotaru

A single dot in your macOS menu bar that tells you whether your CLI coding agents are still working or waiting on you.

1

stars

1

commits

Python

primary language

Sep 7, 2026

updated

ai
ai-agents
claude-code
cursor-cli
macos
menubar
swiftbar

README

Hotaru

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 · The name · How it works · Things worth knowing


The problem

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.

Hotaru in the macOS menu bar: a red dot with a 1/3 badge, and a dropdown listing three agent sessions with their state, path and elapsed time.

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.

DotMeaning
Grey, hollowNo sessions running
RedAt least one agent is thinking
GreenEvery 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.

The name

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

How it works

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.

Eventcursor-agentClaude CodeResult
Session openssessionStartSessionStartgreen
You submit a promptbeforeSubmitPromptUserPromptSubmitred
A tool call beginspreToolUsePreToolUsered
The turn finishesstopStopgreen
Session closessessionEndSessionEndremoved

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

Requirements

  • macOS (Monterey or later, as SwiftBar requires)
  • SwiftBarinstall.sh will install it via Homebrew if missing
  • python3
  • At least one of Cursor CLI or Claude Code

Install

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/ 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

Customising

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 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"

Things worth knowing

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.

Troubleshooting

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

./uninstall.sh

Removes the hook entries, the plugin and ~/.hotaru, leaving SwiftBar installed and your config backups in place.

Possible additions

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.

License

MIT — see LICENSE.

Contributors

ajwadjaved

1 commits

ajwadjaved/hotaru

A single dot in your macOS menu bar that tells you whether your CLI coding agents are still working or waiting on you.

1

stars

1

commits

Python

primary language

Sep 7, 2026

updated

ai
ai-agents
claude-code
cursor-cli
macos
menubar
swiftbar

README

Hotaru

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 · The name · How it works · Things worth knowing


The problem

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.

Hotaru in the macOS menu bar: a red dot with a 1/3 badge, and a dropdown listing three agent sessions with their state, path and elapsed time.

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.

DotMeaning
Grey, hollowNo sessions running
RedAt least one agent is thinking
GreenEvery 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.

The name

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

How it works

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.

Eventcursor-agentClaude CodeResult
Session openssessionStartSessionStartgreen
You submit a promptbeforeSubmitPromptUserPromptSubmitred
A tool call beginspreToolUsePreToolUsered
The turn finishesstopStopgreen
Session closessessionEndSessionEndremoved

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

Requirements

  • macOS (Monterey or later, as SwiftBar requires)
  • SwiftBarinstall.sh will install it via Homebrew if missing
  • python3
  • At least one of Cursor CLI or Claude Code

Install

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/ 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

Customising

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 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"

Things worth knowing

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.

Troubleshooting

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

./uninstall.sh

Removes the hook entries, the plugin and ~/.hotaru, leaving SwiftBar installed and your config backups in place.

Possible additions

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.

License

MIT — see LICENSE.

Contributors

ajwadjaved

1 commits

Languages

Python

81.1%

Shell

18.9%