Know when Claude needs you — without alt-tabbing.
Promptly is a tiny, movable, always-on-top status light for macOS & Linux. By default it's a slick spinner — it spins while busy, pulses when it needs you, and settles into a ✓ when it's done:
| Spinner | State | Meaning |
|---|---|---|
| Busy | working | |
| Needs you | needs your input | |
| Ready | ready / idle |
Prefer something else? Switch looks in one click — orb, breathing gradient, classic traffic light, or a little Tamagotchi (see Skins).
It's driven by a dead-simple local HTTP server, so anything that can run a shell command or hit a URL can control it — Claude Code, shell scripts, CI, cron, a Slack bot, your imagination. Red while it works, yellow when it needs you, green when it's done. Click it to jump straight back to where Claude is running.
The colors are just states — wire them to whatever you like.
One line — fetches Promptly, sets up a private virtualenv (PySide6 won't touch your
system Python; uses uv if it's installed, with live
download progress), and puts promptly on your PATH:
curl -fsSL https://raw.githubusercontent.com/AssenDimitrov/promptly/main/install.sh | bashThen just run it:
promptly # starts in the background, parks bottom-right
promptly --skin orb # pick another look (see "Skins")promptly launches detached — it won't hold your terminal — and prints how to
connect it to Claude Code. Logs go to $TMPDIR/promptly.log; stop it with the
printed kill <pid>, or right-click the light → Quit.
Installs into ~/.local (override with PROMPTLY_PREFIX). Re-run to update.
Uninstall with rm -rf ~/.local/share/promptly ~/.local/bin/promptly.
Prefer to read before you pipe to a shell? It's a tiny script —
install.sh. Or run from source (below).
pipx install git+https://github.com/AssenDimitrov/promptly # then: promptly
uv tool install git+https://github.com/AssenDimitrov/promptly # (uv alternative)git clone https://github.com/AssenDimitrov/promptly && cd promptly
pip install -r requirements.txt # just PySide6-Essentials (or: pip install PySide6-Essentials)
python promptly.py # spinner skin (default), parks bottom-right
python promptly.py --skin orb # pick another look (see "Skins")
python promptly.py --skin tamagotchi --scale 1.4
python promptly.py --vertical # classic vertical stoplight
python promptly.py --state red --port 7654Then flip it from anywhere:
curl -s localhost:7654/red
curl -s 'localhost:7654/yellow?flash=1&label=Needs+your+input'
curl -s localhost:7654/green- Left-click → jumps to the window where Claude is running (see Claude Code).
- Drag (press and move) → reposition it anywhere.
- Right-click → all the options (skin, size, orientation, opacity, always-on-top, show-over-fullscreen, and more).
It floats above other windows and stays out of the Dock/taskbar.
Linux note: needs a Qt platform plugin. On a normal desktop session it works out of the box; on Wayland you may want
QT_QPA_PLATFORM=wayland(orxcbto force X11).
Same three states, five different vibes. Switch any time via right-click → Skin,
or start with --skin <name>:
| Skin | --skin |
Look |
|---|---|---|
| Spinner | spinner (default) |
indeterminate spinner while busy, a ✓ when ready |
| Orb | orb |
a single breathing orb |
| Breathing gradient | gradient |
an ambient bar that breathes; great "attached to top" |
| Traffic light | traffic |
the classic three lamps |
| Tamagotchi | tamagotchi |
a creature that works, calls you, and rests |
A skin is purely a render strategy over the state — the HTTP server and your Claude Code hooks are identical for every skin. Adding your own is one class (see How it's built).
👉 See them move at the bottom of this page.
Talk to Promptly over http://127.0.0.1:7654. Simple path endpoints:
curl -s localhost:7654/red
curl -s 'localhost:7654/yellow?flash=1&label=Needs+your+input'
curl -s localhost:7654/green
curl -s localhost:7654/off
curl -s localhost:7654/state # read current state as JSON
curl -s 'localhost:7654/focus?app=iTerm' # macOS: set click target
curl -s 'localhost:7654/focus?title=claude-code' # Linux/X11: set click targetOr richer JSON via POST:
curl -s localhost:7654/state \
-d '{"state":"yellow","flash":true,"label":"Running tests"}'That HTTP endpoint is the whole extensibility story: GitHub Actions, a deploy script, a long-running job, a Slack bot — any of them can flip the light with one line.
Claude Code fires lifecycle hooks you can map straight onto the light. Open
claude-code-settings.json (installed at ~/.local/share/promptly/claude-code-settings.json),
copy its hooks block into either:
~/.claude/settings.json— applies to every project, or<project>/.claude/settings.json— just this repo.
The mapping:
| Claude Code event | Fires when… | Light |
|---|---|---|
SessionStart |
a session begins | 🟢 green |
UserPromptSubmit |
you send a prompt | 🔴 red (working) |
PreToolUse |
it's about to run a tool | 🔴 red (working) |
Notification |
it needs input / a permission | 🟡 yellow flashing |
Stop |
it finishes the turn | 🟢 green (ready) |
SessionEnd |
the session ends | ⚫ off |
Each hook is just curl -s -m 1 localhost:7654/<color> || true, run with
async: true — so if Promptly isn't running, Claude Code is never blocked or
slowed. Start Promptly, start Claude Code, and the light tracks it live.
Left-clicking the light raises the window Claude Code lives in. The SessionStart
hook runs register-claude-window.sh, which auto-detects where to focus:
- macOS — detects the terminal from
$TERM_PROGRAM. For Terminal.app and iTerm2 it captures the exact window Claude started in (it's the frontmost one at session start) and re-raises precisely that window — handy when several terminal windows are open. Other terminals (VS Code, Ghostty, WezTerm, Hyper, …) fall back to focusing the app by name. The first click triggers a one-time macOS prompt — "Python wants to control Terminal/iTerm" — click Allow. - Linux/X11 — focuses a window by matching its title with
wmctrl/xdotool(apt install wmctrlorxdotool). If your terminal has no useful title, set one in your shell rc withprintf '\033]0;claude-code\007'andexport CLAUDE_WINDOW_TITLE=claude-code. Wayland generally blocks one app from focusing another's window — there it may not work.
Edit /path/to/trafficlight in the SessionStart hook to point at this folder
(after the one-line install that's ~/.local/share/promptly). You can also set the
target by hand any time: right-click → Claude window → Set target…, and test it
with Claude window → Focus now.
Everything is configurable here:
- Skin → Spinner / Orb / Breathing gradient / Traffic light / Tamagotchi
- Orientation → Horizontal / Vertical (for skins that support it)
- Size → Small / Medium / Large / Extra large
- Opacity → 100% / 85% / 70% / 50%
- Attach to top bar → snaps flush to the top edge and locks it there
- Always on top → float above other windows
- Sound → play a short sound on each state change — started (red), needs you
(yellow), done (green). Off by default; start with
--soundto enable at launch - Show on all Spaces & over fullscreen apps (macOS) → appear on every Space and over other apps' fullscreen windows (on by default)
- Claude window → Focus now, Set target…, and the current target
- Quit
To behave like a proper always-visible status indicator, on macOS Promptly reaches
past Qt to the native NSWindow/NSApplication (via ctypes against the
always-present libobjc — no extra dependency) and:
- stays above other apps at a high window level,
- rides along to every Space/desktop (
CanJoinAllSpaces), - stays visible when another app is frontmost (Qt's
Qt.Toolwould otherwise hide it on deactivation), - drops its Dock icon by running as an accessory app — which is also what lets it float over other apps' fullscreen Spaces.
Overlaying another app's native fullscreen is the one thing public macOS APIs
won't do, so it uses a private Spaces API — undocumented and may change across
macOS releases. It's on by default; turn it off with right-click → Show on
all Spaces & over fullscreen apps, or PROMPTLY_FORCE_ALL_SPACES=0. Everything
else uses public APIs only.
| Variable | Default | Effect |
|---|---|---|
PROMPTLY_FORCE_ALL_SPACES |
on | =0 stops it showing over other apps' fullscreen Spaces (private API; also a menu toggle). |
PROMPTLY_LEVEL |
1000 |
Window level. 1000 floats over fullscreen; 25 = status level (below the menu bar) if 1000 feels too aggressive. |
PROMPTLY_KEEP_DOCK |
off | =1 keeps the Dock icon (you then lose the over-fullscreen overlay). |
PROMPTLY_NO_ORDERFRONT |
off | =1 skips the periodic re-raise (rarely needed). |
PROMPTLY_DEBUG |
off | =1 prints one diagnostic line about the native window state to stderr. |
TL_PORT |
7654 |
Control port used by register-claude-window.sh (match --port). |
# floats over everything (incl. other apps' fullscreen) out of the box;
# pass =0 to keep it off other apps' fullscreen Spaces
PROMPTLY_FORCE_ALL_SPACES=0 python promptly.py --skin spinnerThese knobs are macOS-only and harmless elsewhere. On Linux the window manager governs always-on-top / across-desktops behavior directly.
any tool (Claude Code hook, script, CI…)
│ HTTP GET/POST → /red /yellow /green /off /state
▼
┌──────────────────────────────────────────────┐
│ promptly.py │
│ ┌────────────┐ Qt signal ┌────────────┐ │
│ │ HTTP server│ ────────────► │ widget │ │
│ │ (thread) │ (thread-safe)│ + skin │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────────────────┘
- The HTTP server runs on a background thread and hands updates to the GUI thread
through a Qt signal (
Bridge.apply) — the safe way to touch the UI from another thread. - Adding a new trigger = make something call the endpoint. No app changes needed.
- Adding a new look = add a
Skinsubclass (size+paint, optionallyanimating) and list it inSKIN_LIST. The control layer is untouched. - Adding a new state (e.g. a 4th lamp) = extend
VALID_STATES, then handle it in the skins you care about.
Built in PySide6 (Qt) because it's one pip-install, one file, no build step,
and looks good on both OSes. The same local-HTTP design ports cleanly to Electron,
Tauri, or a rumps menu-bar app if you'd rather — and your Claude Code hooks
wouldn't change.
Same three states (busy → needs you → ready), five different personalities. The spinner is up top; here are the other four, each shown live in a different state so you can see how they read at a glance:
|
Orb a single breathing glossy sphere |
Tamagotchi a creature that works, calls you, and rests |
|
Traffic light the classic three lamps |
Breathing gradient an ambient bar — great snapped to the top edge |
Switch between them any time via right-click → Skin, or start with
--skin orb / gradient / traffic / tamagotchi.
MIT — do whatever you like.

