A live terminal dashboard that shows every running Claude Code and Codex
instance and its state, so you don't have to switch between projects to find the
one stuck on a yes / don't ask again permission prompt. Select a session and
hit Enter to jump straight to its window.
git clone https://github.com/serjou1/adhd.git ~/.claude/adhd
python3 ~/.claude/adhd/install.pyThe installer drops an adhd command on your PATH and wires the Claude Code
hooks. (The repo is private — git clone uses your existing GitHub credentials.)
adhdThat's it. It refreshes every second and picks up every Claude Code session
automatically. No setup per project. Runtime state lives in ~/.adhd/state
(override with ADHD_STATE_DIR).
adhd tracks Codex (codex CLI) sessions too — with zero setup. Codex has
no hook system like Claude Code's, so instead of being told, adhd polls: every
refresh it finds the live codex processes and reads each one's rollout
transcript (~/.codex/sessions/…) to work out what it's doing and what it's about.
Codex rows are tagged cx (Claude rows are cc) so you can tell them
apart at a glance, in both the dashboard and the menu bar. Everything else is the
same — state, window focus, notifications, and recently closed with resume
(r runs codex resume <id>, falling back to codex resume --last).
| Signal | State |
|---|---|
rollout: task_started, agent message / reasoning, tool call, web search |
working |
| rollout: a usage / rate limit | limit |
rollout: task_complete / turn_aborted |
idle |
terminal title shows Action Required (an approval prompt) |
waiting |
Codex only writes its conversation to the rollout — approval prompts never
land there, so adhd reads those from the terminal tab title (the [ ! ] Action Required Codex sets when it needs your yes/no). That title check runs on
Terminal.app and iTerm2; on other terminals a Codex session still shows
working/idle from the rollout, just without the red waiting flag and badge.
Because detection is by polling, a Codex session shows up only while adhd or
adhd-menu is running — there's nothing to install into Codex and nothing left
behind. It doesn't matter how Codex was launched (codex, a codex-fugu
wrapper, …) — adhd matches the underlying codex process. Set
CODEX_SESSIONS_DIR if your rollouts don't live in ~/.codex/sessions.
Don't want a whole terminal pane? adhd-menu puts a status-bar icon up top with
a badge counting the sessions WAITING on a permission prompt — the ones that
literally need your click. ◧ 2 means two sessions are blocked. Next to the
badge it shows the chat title of the session that needs you most (Claude's
own conversation title, or a Codex session's latest prompt), so you can see
what's waiting without opening the menu — and each dropdown row pairs the
project folder with its chat title (proj · title), tagged cc / cx for the
agent.
adhd-menu # run it now
python3 install.py --login # ...or auto-start it at loginClick the icon for a live menu:
1 waiting · 1 limited · 2 working · 4 idle
───────────────────────────────
🔴 cc arbitrage-core — needs your permission (12s)
🟣 cc backtester — rate-limited (4m)
🟡 cx statistics-service — responding (1s)
🟢 cc twitter-crawler — waiting for input (47s)
───────────────────────────────
Open adhd monitor…
✓ Notifications
✓ Repeat reminders (10 min)
Auto-resume rate-limited
───────────────────────────────
Quit
Pick any session to bring its window to the front (same focus logic as the
dashboard — tmux pane, iTerm/Terminal tab, or VS Code window). Open adhd
monitor… launches the full curses dashboard in a new Terminal window. The
badge hides when nothing is waiting. Override the glyph with ADHD_MENU_ICON.
While adhd-menu is running it pops a macOS notification when a session needs
you, so you can work in one window and get pulled back to another only when it
matters:
| When | Notification |
|---|---|
| A session finishes its turn (working → idle) | ✅ project — done |
| A session blocks on a permission prompt (needs access) | 🔴 project needs you |
| A blocked session is still waiting after 10 min (repeat reminders on) | 🔴 project still needs you |
| A session hits a usage / rate limit (waiting for reset) | 🟣 project rate-limited |
Toggle notifications from the menu (Notifications), or start muted with
ADHD_NOTIFY=0. No burst on startup — already-running sessions are seeded
silently and only transitions after that fire a toast.
Repeat reminders (on by default) nudge you again every 10 minutes about a
session that's still blocked on a prompt — so a banner you missed comes back
instead of being lost. The moment the session stops waiting (you approve, or it
moves on) the reminders stop. Toggle them from the menu (Repeat reminders),
or start them off with ADHD_NOTIFY_REPEAT=0; change the interval (seconds)
with ADHD_NOTIFY_REPEAT_SECS.
Off by default. Turn it on from the menu (Auto-resume rate-limited) —
the choice is saved to ~/.adhd/config.json, so it sticks across restarts and
reboots. adhd then picks a stalled session back up for
you: when a session is blocked on a usage / rate limit, it waits out a cooldown,
confirms the network is reachable, then types a short prompt into that exact
session to push it forward. So a limit you hit while offline resumes when the
connection returns, and a usage cap resumes after it resets — without you
babysitting the terminal.
It opts in because it types into your terminal. To stay safe it only nudges sessions it can target precisely — a tmux pane, an iTerm session id, or a Terminal tab matched by tty — so a keystroke can never land in the wrong window. VS Code sessions (no scriptable terminal) are left alone. A session that's still limited is retried after each cooldown, not hammered every tick, and only the first nudge of an episode toasts (↩️ project — resuming).
| Variable | Default | Meaning |
|---|---|---|
ADHD_AUTO_RESUME |
0 (off) |
Seeds the first run only. The login-item app runs under launchd, which ignores your shell env, so this var never reaches it — enable it with the menu toggle (which now persists) |
ADHD_RESUME_TEXT |
continue |
The prompt typed into the session to resume it |
ADHD_AUTO_RESUME_SECS |
300 |
Cooldown between retries while still limited (seconds) |
Clicking a toast brings the waiting session's window to the front. To land on a specific one when several need you, click the menu-bar icon and pick it there.
How the click works. A macOS banner is "owned" by whatever app calls
display notification, and clicking it opens that app. A bareosascripttoast is owned by Script Editor — which is why clicks used to pop it open. So on first useadhdbuilds a tiny applet at~/.adhd/adhd.app(viaosacompile, brandedcom.adhd.notifierand ad-hoc signed) and posts every banner through it. The banner is now owned by adhd, and clicking it relaunches the applet, which focuses a waiting session (menubar.py --focus). The applet rebuilds itself if deleted; the first banner from it may need approving once in System Settings → Notifications → adhd.The modern
UNUserNotificationCenterAPI could target the exact window per banner, but it needs a fully packaged, signed.app; the old CLI tools (terminal-notifier,alerter) are dead on macOS 14+ — they use the removedNSUserNotificationAPI and silently never appear.
Needs the
rumpspackage (a tiny PyObjC wrapper).install.pyinstalls it for you; otherwisepip3 install --user rumps.
Agent Monitor (Claude · Codex)
5 running 1 waiting 1 limited 2 working 1 idle
STATE PROJECT DETAIL AGE CWD
WAITING cc upbit-crawler needs permission to use... 12s /Users/serjou/upbit-crawler
LIMIT cc backtester usage limit reached 4m /Users/serjou/backtester
WORKING cx arbitrage-core responding 3s /Users/serjou/arbitrage-core
WORKING cc statistics-service Bash 1s /Users/serjou/statistics-service
IDLE cc twitter-crawler waiting for input 47s /Users/serjou/twitter-crawler
The cc / cx prefix on each PROJECT is the agent — cc Claude Code,
cx Codex.
| State | Meaning | What to do |
|---|---|---|
| WAITING (red) | Blocked on a permission / approval prompt — needs your click | Go to that one |
| LIMIT (magenta) | Hit a usage / rate limit — blocked on the clock, not on you | Wait for reset, or switch session |
| WORKING (yellow) | Actively processing a prompt or running a tool | Leave it alone |
| IDLE (green) | Finished its turn, sitting at the prompt for your next message | Free when you are |
Rows are sorted so WAITING is at the top, then LIMIT, then the rest.
| Key | Action |
|---|---|
↑ / ↓ (or k / j) |
Move the selection (spans live sessions and recently-closed rows) |
⏎ Enter |
Live row → focus its window. Recently-closed row → open it fresh |
r |
Recently-closed row → resume its previous conversation (⇧⏎ works too on modifier-reporting terminals; see below) |
c |
Clear stale entries (sessions older than 6h that never sent a clean exit) |
q |
Quit the dashboard |
Select a row and press Enter to bring its terminal to the front. The hook records how each session was launched and the dashboard picks the best focus method:
| Terminal | How it focuses | Precision |
|---|---|---|
| tmux | tmux select-window + select-pane on the recorded pane |
exact pane |
| iTerm2 | AppleScript match on ITERM_SESSION_ID |
exact tab/session |
| Terminal.app | AppleScript match on the tab's tty |
exact tab |
| VS Code (one window per session) | code <project-root> focuses the window that has that folder open |
exact window |
| other | activates the app (can't target the exact pane) | app only |
The result of each jump is shown just above the footer. Sessions that were already running before this feature was installed have no captured window info — restart them (or just let them fire one more event) and they'll become jumpable.
VS Code jumping uses the
codeCLI — no Accessibility permission needed.code <folder>brings an already-open window for that folder to the front. The target folder is the session's git root (so a session started in a deep subdir likecrates/foo/srcstill resolves to thearbitrage-corewindow), falling back to the cwd for non-git folders. This is also why thePROJECTcolumn shows the repo name instead of the subdir name. Requires thecodecommand on your PATH — in VS Code run Shell Command: Install 'code' command in PATH if it's missing.
Below the live sessions the dashboard shows a recently closed section: the
last 10 distinct projects whose sessions have ended. Select one to re-open it
where it lived — VS Code sessions re-open the project folder in VS Code
(code <root>, the window the session ran in); iTerm / Terminal sessions open a
fresh window in that app, cd'd into the project root.
Two ways to re-open, so resuming is a deliberate choice:
| Key | Does |
|---|---|
⏎ |
Open a fresh claude (or codex) in the project (a clean slate) |
r |
Resume — claude --resume <session_id> (or codex resume <session_id> for a Codex row), bringing back the exact previous conversation. If that conversation is gone (transcript cleared/rotated, or an older entry with no saved id), it falls back to claude --continue / codex resume --last (the latest conversation in that project) instead of erroring out (⇧⏎ also works on modifier-reporting terminals) |
The session id is remembered in the history entry (alongside the project root
and the terminal it ran in), which is what makes resume possible. VS Code
sessions always just open the folder — resume claude yourself in its
integrated terminal, where the session lived.
Heads-up on
⇧⏎: most terminals (Terminal.app, default iTerm2) send the same byte for⏎and⇧⏎, so they can't be told apart unless the terminal reports modifier keys (kitty, or iTerm2/xterm with CSI-u / modifyOtherKeys enabled).ralways works — use it if⇧⏎opens fresh on your terminal.
A project is recorded as closed whether it ended cleanly (SessionEnd) or was
killed/force-closed (the reaper notices its terminal is gone). The list lives in
~/.adhd/history.json on disk, so it survives restarts, updates, and power
loss: whatever was open before a crash lands in history the next time the
dashboard runs and reaps its leftover state file. (Leftover files from before the
last reboot are reaped on sight — their tty may have been reused, so trusting
it would risk a stale or mis-targeted row.) A project that's open again is hidden
from the list until it closes once more.
- Claude Code fires hooks on lifecycle events (prompt submitted, tool about to run, permission notification, turn finished, session ended).
hook.pyruns on each event and writes a small JSON state file to~/.adhd/state/<session_id>.json.monitor.pypolls that folder once a second and renders the table.
Event → state mapping (in hook.py):
| Hook event | State it sets |
|---|---|
SessionStart |
idle |
UserPromptSubmit |
working |
PreToolUse |
working (tool name) |
PostToolUse |
working |
Notification |
waiting (permission), limit (usage/rate limit), or idle (idle prompt) |
Stop |
idle (done) |
SessionEnd |
removed from dashboard, recorded in recently closed |
Codex has no hooks, so it's handled the other way around: codex.py polls
the live codex processes and their rollout files on each refresh and builds the
same records on the fly (tagged tool: "codex"), which flow through the same
loading, focus, and notification code. See Codex sessions above for its event
mapping.
| Path | Role |
|---|---|
install.py |
One-shot installer: adds the adhd / adhd-menu commands, wires the hooks, installs rumps (--login adds a LaunchAgent). |
hook.py |
Claude Code event handler; writes per-session state, records closes to history. Always exits 0 so it can't break a session. |
codex.py |
Polls live Codex sessions (process list + rollout files) into the same records, so Codex shows up with no hooks. |
monitor.py |
The terminal dashboard (also the shared session-loading / window-focus / re-open layer). |
menubar.py |
The macOS menu-bar app. Reuses monitor.py's loading + focus logic. |
history.py |
Recently-closed project store (load / record). Shared by hook.py, codex.py, and monitor.py. |
~/.codex/sessions/ |
Codex's own rollout transcripts — adhd reads them to detect Codex state (override with CODEX_SESSIONS_DIR). |
~/.adhd/state/ |
One JSON file per live session (override with ADHD_STATE_DIR). |
~/.adhd/history.json |
The last 10 closed projects, for re-opening (survives restarts/power loss). |
~/.claude/settings.json |
Holds the global hooks block that wires the events to hook.py. |
- A session isn't showing up? Sessions already open before the hooks were
installed may not report. Restart that
claudeand it will appear. New sessions are picked up automatically. - A permission prompt didn't turn a row red? The "waiting" detection relies
on the
Notificationhook. If a prompt isn't classified as waiting, adjust the keyword matching in theclassify()function inhook.py. - A crashed/force-closed session lingers. It never sends
SessionEnd, but the monitor auto-reaps it: each refresh it drops any session whose terminal (tty) no longer has a liveclaudeprocess (and any left over from before the last reboot). Reaped sessions are recorded in recently closed so you can re-open them. Sessions whose tty wasn't captured (or if the process list can't be read) are kept, andcstill clears anything with no update for 6h as a backstop. - A Codex session isn't showing? Codex is detected by polling, so it only
appears while
adhdoradhd-menuis running (and the row updates each refresh). Make sure thecodexprocess is alive and its rollout is under~/.codex/sessions— or pointCODEX_SESSIONS_DIRat the right place. There's nothing to install into Codex itself. - "No sessions reporting yet." Normal when no Claude Code or Codex instances are running, or no Claude session has fired an event since install.


