Ampel: a menu bar traffic light for Claude Code

The failure mode of running Claude Code in a terminal is simple: you give it a task, it starts working, you tab away to something else, and it stops thirty seconds later on a permission prompt. You come back four minutes later to a session that has done nothing since. Run three sessions across three repositories and this happens constantly.
The information you need is tiny. Is anything waiting for me? That is one bit, and it does not deserve a window. So I built Ampel, a macOS menu bar app that renders it as a traffic light. “Ampel” is German for traffic light, which is what I look at all day here in Braunschweig anyway.
| Color | State | Meaning |
|---|---|---|
| Red | attention | Claude is blocked on a decision from you: a permission prompt, or a tool asking for input |
| Yellow | working | Claude is actively processing, tools running or a response streaming |
| Green | idle | The last turn finished, nothing pending |
| Gray | off | No active Claude Code sessions |
Several sessions aggregate worst-case: red beats yellow beats green. If any one of them is blocked, the icon is red. Click it, with either mouse button, and you get a menu listing each session with its project, its state and when it last did something, plus a usage section. Sessions that share a project name get a short session id appended, so two parallel sessions in the same repo stay tellable apart.
Install
Homebrew, three commands:
brew trust appgineering/tap
brew tap appgineering/tap
brew install --cask ampel
The brew trust line comes first and it is not optional. Homebrew 6 refuses to load casks from a tap you have not explicitly trusted, and it reports that refusal as a syntax error, which sends you looking for a bug in the cask that is not there. Trust the tap first and the other two commands behave normally.
To update later:
brew update
brew upgrade --cask ampel
To remove it, including the tap:
brew uninstall --cask ampel
brew untap appgineering/tap
Ampel keeps its own state in ~/.ampel and its hooks in ~/.claude/settings.json, and Homebrew does not touch either. Turn the hooks off from the app’s settings before uninstalling, or delete ~/.ampel and remove the ampel-hook entries from ~/.claude/settings.json by hand afterwards.
If you would rather not use Homebrew, download the latest release, unzip it, and drag Ampel.app into /Applications. It needs macOS 14 or newer.
First launch
The icon starts gray, because nothing is tracked until the hooks exist. Open the menu and a setup guide walks you through installing them: it writes a small shell script to ~/.ampel/bin/ampel-hook and merges a hooks block into ~/.claude/settings.json. Your existing settings and any other tool’s hooks are preserved, and a timestamped backup of the file is written before anything changes.
Start a Claude Code session after that and the icon should go yellow within a second of the first tool call.
How it works
Claude Code fires shell commands at lifecycle events, with a JSON payload on stdin. Ampel’s hook is about ten lines of bash: read stdin, wrap it in an envelope with the event name and a timestamp, write it to a temp file, rename it into ~/.ampel/events/. The app watches that directory with a DispatchSource, drains new files, updates its per-session state machine, and deletes each file once it has been applied.
Claude Code ──hook──▶ ~/.ampel/events/<ts>-<pid>-<rand>.json
│
Ampel.app ── directory watch ──▶ parse → update session → render icon
The obvious alternative was a localhost server with the hook posting to it. The spool is better on every axis I care about. It works when the app is launched after the sessions started. It survives an app restart without losing events. And the hook path has no dependencies at all: no jq, no Python, no curl, just /bin/bash and mv. Writing to a temp file and renaming means the watcher never reads a half-written file, and the hook always exits 0, so a broken Ampel can never block a Claude Code session.
One decision took a while to get right. Claude Code also fires a notification when a session has been sitting at an empty prompt for a while. Treating that as attention seemed correct until I tried it: the icon pinned itself red for as long as any session was merely open, which is most of the day, and a light that is always red is not a light. Now red means exactly one thing, that a decision is waiting.
Usage numbers
The menu shows usage from two sources that measure different things, and it is worth being precise about which is which.
Real plan limits are the same percentages /usage prints. Claude Code passes them on the statusLine hook’s stdin, so this is opt-in under Settings > Usage: turning it on installs a statusLine wrapper that chains onto whatever statusline you already run and restores the original when you turn it back off. Per-model figures are not in that payload, so Ampel cannot show them.
Estimated cost comes from ccusage, which reads the JSONL transcripts already under ~/.claude/projects/. It is optional and the app degrades to a single line without it. On a subscription plan the dollar figure is notional, what those tokens would have cost on the API, so it will never line up with the plan percentages. Two numbers, two meanings, both shown rather than averaged into something misleading.
Settings
An ordinary preferences window with General, Usage and About panes. Launch at login. Whether the icon pulses when something needs attention, and whether a blocked session also raises a notification. Six menu bar icon styles, previewed live, because a menu bar is a personal space. How the usage section is drawn: bars, numbers only, or hidden.
Privacy
Ampel collects nothing, sends nothing, and makes no network requests of its own. There is no analytics, no crash reporting service and no account. It reads the event spool it wrote itself, ~/.claude/settings.json to check whether its hooks are installed, and its own log and usage cache.
The one thing worth knowing: creating ~/.ampel/debug makes the hook append every event payload verbatim to ~/.ampel/hook.log, and those payloads include your prompt text. It is off by default and exists only for diagnosing a lost event, since the app deletes each spool file once it has applied it. Delete the flag file and the log when you are done. The full detail is in PRIVACY.md.
Building it yourself
The Xcode project is generated from project.yml with XcodeGen and is not checked in:
brew install xcodegen
git clone https://github.com/appgineering/Ampel.git
cd Ampel
xcodegen generate
open Ampel.xcodeproj
./Tests/run.sh runs the self-checks: the session state machine, the ccusage and plan-usage parsers, and the hook installer’s merge behavior against throwaway home directories. That last one matters more than it sounds, because the installer edits a file you did not write and cannot afford to clobber.
Ampel is MIT licensed and lives at github.com/appgineering/Ampel. It is not affiliated with or endorsed by Anthropic; Claude and Claude Code are trademarks of Anthropic, PBC.
Built at Appgineering, the studio I co-founded.