{"slug": "hotaru-single-dot-in-your-macos-menu-bar-showing-cli-coding-agents-status", "title": "Hotaru – Single dot in your macOS menu bar showing CLI coding agents status", "summary": "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.", "body_md": "A single dot in your macOS menu bar that tells you whether your CLI coding agents are\n\nstill working or waiting on you. Red means thinking. Green means free.\n\n[Install](#install) ·\n[The name](#the-name) ·\n[How it works](#how-it-works) ·\n[Things worth knowing](#things-worth-knowing)\n\nIf 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.\n\nHotaru puts that answer in the corner of your screen. One glance, no pane-hopping.\n\nThe 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.\n\n| Dot | Meaning |\n|---|---|\n| Grey, hollow | No sessions running |\n| Red | At least one agent is thinking |\n| Green | Every session is free and waiting on you |\n\nThe badge disambiguates when you have several sessions. `1/3`\n\nmeans one of three is thinking; a bare `3`\n\nmeans all three are in the same state; a single session shows no number at all.\n\n*Hotaru* (蛍) is Japanese for **firefly**.\n\nIt 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.\n\nNo screen-scraping and no polling of the agents. Both CLIs expose a hook system, so Hotaru asks them directly.\n\n```\ncursor-agent ─┐\n              ├─→ ~/.hotaru/report.py ─→ ~/.hotaru/sessions/*.json ─→ SwiftBar plugin ─→ ●\nClaude Code ──┘\n```\n\n`report.py`\n\nis 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.\n\n| Event | cursor-agent | Claude Code | Result |\n|---|---|---|---|\n| Session opens | `sessionStart` |\n`SessionStart` |\ngreen |\n| You submit a prompt | `beforeSubmitPrompt` |\n`UserPromptSubmit` |\nred |\n| A tool call begins | `preToolUse` |\n`PreToolUse` |\nred |\n| The turn finishes | `stop` |\n`Stop` |\ngreen |\n| Session closes | `sessionEnd` |\n`SessionEnd` |\nremoved |\n\nThe plugin renders those files. Because hooks fire on state changes, `report.py`\n\nalso 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.\n\nTwo properties worth calling out, since this code runs inside your agent's critical path:\n\n**It cannot block or alter an agent.**`report.py`\n\nswallows every exception and always exits`0`\n\nwith 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`\n\ned 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.\n\n- macOS (Monterey or later, as SwiftBar requires)\n[SwiftBar](https://swiftbar.app)—`install.sh`\n\nwill install it via Homebrew if missing`python3`\n\n- At least one of\n[Cursor CLI](https://cursor.com/cli)or[Claude Code](https://claude.com/product/claude-code)\n\n```\ngit clone https://github.com/ajwadjaved/Hotaru.git\ncd Hotaru\n./install.sh\n```\n\nThe installer copies the reporter to `~/.hotaru/`\n\n, 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.\n\nYour existing config is **merged, not replaced** — `~/.claude/settings.json`\n\nin 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.\n\nAfter 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.\n\n## Manual install\n\n```\nmkdir -p ~/.hotaru/sessions\ninstall -m 755 src/report.py ~/.hotaru/report.py\ninstall -m 755 src/hotaru.5s.py ~/.swiftbar/hotaru.5s.py   # or your SwiftBar plugin folder\n```\n\nThen add the hook entries from [ examples/](/ajwadjaved/Hotaru/blob/main/examples) to\n\n`~/.cursor/hooks.json`\n\nand `~/.claude/settings.json`\n\n. Use absolute paths — those commands are not shell-expanded, so `~`\n\nand `$HOME`\n\nwon't resolve. Or just run the merge step on its own:\n\n```\npython3 src/wire_hooks.py\n```\n\nEverything is two short Python files; edit them in place.\n\n**Colours** are the constants at the top of`hotaru.5s.py`\n\n. They're[Kanagawa Wave](https://github.com/rebelot/kanagawa.nvim)by default, so they match a typical Neovim or starship setup.`SAMURAI_RED`\n\nis quite hot in a menu bar —`#C34043`\n\nis a calmer alternative.**Refresh interval** is the filename. Rename to`hotaru.10s.py`\n\nto halve the background work; state changes still appear instantly via the URL-scheme nudge.**Dropdown contents** are the`main()`\n\nfunction.\n\nAfter editing the plugin, refresh it:\n\n```\nopen -g \"swiftbar://refreshplugin?name=hotaru\"\n```\n\nFour non-obvious behaviours, all of which cost real debugging time and are baked into the code now.\n\n**Hooks load at startup.** Neither CLI re-reads its hook config mid-session, so changes only take effect in new sessions.\n\n** -p print mode fires no hooks.** Running\n\n`cursor-agent -p \"...\"`\n\nnon-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`\n\nworks well.**A hook's parent process is not the agent.** Hooks are spawned through a short-lived shell that exits immediately, so `getppid()`\n\nis useless for liveness. `report.py`\n\nwalks 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.\n\n**SwiftBar renders sfimage as a template image.** That forces it monochrome, so\n\n`sfcolor`\n\nis silently ignored and you get a white blob. Hotaru draws a `●`\n\ntext character coloured with `color=`\n\ninstead, which can't be overridden. `sfcolor`\n\nonly applies to SF Symbols embedded in the title text via `:symbol:`\n\nsyntax.**The dot stays hollow grey.** Confirm a session actually registered:\n\n```\nls ~/.hotaru/sessions/          # one JSON file per live session\ncat ~/.hotaru/events.log        # every hook fired, with the resolved agent pid\n```\n\nAn empty events log means the hooks aren't wired or the session predates them. Start a fresh session and check again.\n\n**The dot is there but never changes colour.** Run the plugin by hand — its output tells you what SwiftBar is being asked to draw:\n\n```\n~/.swiftbar/hotaru.5s.py\n```\n\n**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`\n\nclears the slate safely.\n\n```\n./uninstall.sh\n```\n\nRemoves the hook entries, the plugin and `~/.hotaru`\n\n, leaving SwiftBar installed and your config backups in place.\n\nA distinct colour for \"waiting for your approval\", which is currently lumped in with red because red means \"not free\". Claude Code's `Notification`\n\nevent 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.\n\nMIT — see [LICENSE](/ajwadjaved/Hotaru/blob/main/LICENSE).", "url": "https://wpnews.pro/news/hotaru-single-dot-in-your-macos-menu-bar-showing-cli-coding-agents-status", "canonical_source": "https://github.com/ajwadjaved/hotaru", "published_at": "2026-09-01 11:53:58+00:00", "updated_at": "2026-09-01 12:22:55.922938+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Hotaru", "Ajwad Javed", "SwiftBar", "Cursor CLI", "Claude Code", "GitHub", "macOS"], "alternates": {"html": "https://wpnews.pro/news/hotaru-single-dot-in-your-macos-menu-bar-showing-cli-coding-agents-status", "markdown": "https://wpnews.pro/news/hotaru-single-dot-in-your-macos-menu-bar-showing-cli-coding-agents-status.md", "text": "https://wpnews.pro/news/hotaru-single-dot-in-your-macos-menu-bar-showing-cli-coding-agents-status.txt", "jsonld": "https://wpnews.pro/news/hotaru-single-dot-in-your-macos-menu-bar-showing-cli-coding-agents-status.jsonld"}}