Mission control for Claude Code and Codex CLI.
A native macOS terminal built for running many coding agents at once —
so you always know which one is blocked, working, or waiting on you.
Four Claude Code agents grouped by project, with a live status dashboard at the bottom (dark theme)
Press ⌘G to jump straight to the next WAIT agent.
Running N coding agents — Claude Code, Codex CLI, or a mix — in a stock terminal turns into tab roulette: which session is blocked on a permission prompt, which is mid-tool, which already finished? You end up Cmd-Tabbing through windows, scanning panes, and missing the one that was waiting on you five minutes ago.
termy pulls that state out of the panes and into a glanceable dashboard.
Every running agent — Claude Code or Codex CLI — gets a chip at the bottom
of the window with its live status: IDLE, THINK, or WAIT. When
something goes WAIT, macOS notifies you — even if termy isn't focused.
One keystroke takes you there.
Other terminals give you tabs. termy gives you projects.
Every pane belongs to a project (its cwd), and the top filter bar shows one
pill per project. Click termy to see only the termy panes. Click ALL to
see everything. Same project → same pastel accent everywhere — pane header,
dashboard chip, filter pill — so a four-agent window stops looking like a
soup of identical black rectangles.
Filter bar with numbered project pills visible while holding ⌘
Hold ⌘ and a number appears on every filter pill. Press the number — you're there. ⌘0 is always All; ⌘1–⌘9 jump straight to that project's pane grid. No tab order to memorize, no cycling through things you don't care about.
Prefer names? ⌘K opens a fuzzy project switcher over every project termy has seen, with pane counts and "last used" timestamps. Picking one restores that project's saved pane grid.
The strip along the bottom of the window is one chip per running agent — Claude Code or Codex CLI, mixed freely. Each chip shows project / branch and a live state:
Dashboard row showing WAIT, WAIT, THINK, and IDLE states
IDLE— agent is at the prompt, nothing running.THINK— tool call in flight.WAIT— blocked on a permission prompt. Your attention needed.
⌘G jumps to the next WAIT pane, wherever it is —
across splits, across project filters. The single most useful keystroke in
the app.
Hold ⌘⌥ and a number appears on each dashboard chip; press the number to focus that pane directly, even if it's in a different project filter.
State shows up everywhere. Pane border, dashboard chip, and a macOS notification — all driven by Claude Code's
Notification and Stop hooks.
Three-pane split layout on a single project Splits, not tabs. Nested
NSSplitViews. Drag to resize. ⌘D for a row split, ⇧⌘D for a column.
⌘/ reveals everything. The full shortcut map — focus, projects, splits, window. No hidden bindings. Dark side-by-side with an editor
Lives next to your editor. Light, Dark, or Match System — View → Appearance switches terminal, chrome, and dashboard together.
- Permission-prompt notifications. Claude goes
WAIT→ macOS pings you, even if termy isn't focused. - Workspace that survives. Splits, project grouping, and cwd are all persisted per project. ⌘K a project and termy rebuilds the pane grid you left it in.
- Real splits. ⌘D row split, ⇧⌘D column. Drag dividers. ⌘⏎ toggles maximize.
- Native, not Electron. AppKit + SwiftTerm. 2 MB signed DMG, ~3.6 MB
installed. Real
UserNotifications, Full Disk Access, titlebar chrome.
First launch asks once — "Install termy hooks into Claude Code?" — and the
app merges its entries into ~/.claude/settings.json, preserving any hooks
you already have. Every entry is tagged _termy_managed: true so uninstall
is surgical, and the previous file is backed up to
settings.json.backup-<ts> before any write.
- If you move the app, the next launch detects the stale path and silently re-registers against the new location.
- To uninstall, use the termy menu → Claude Code Hooks... → Uninstall. Your own hooks are untouched.
termy-hookalways exits0and write-timeouts after 100 ms — it cannot stall Claude Code even if termy isn't running.
Codex CLI is supported via the same termy-hook binary and an analogous
TOML installer that writes to ~/.codex/config.toml. If termy detects a
Codex install (it probes ~/.codex and the common Homebrew / npm / cargo /
mise / asdf install paths), it will offer to install the integration on
first launch; otherwise opt in any time via termy menu →
Codex Hooks...
Mappings (per developers.openai.com/codex/hooks):
| Codex event | Pane state |
|---|---|
SessionStart |
hard reset to IDLE |
PreToolUse / PostToolUse |
THINK |
PermissionRequest |
WAIT + macOS notification |
Stop |
IDLE after 30 s |
Codex has no native SessionEnd hook; termy synthesizes one by watching
the foreground process group of each pane's PTY (tcgetpgrp + libproc).
Once codex leaves the foreground (/exit, Ctrl-C), the chip resets to
neutral INIT automatically — even though no hook fired.
Claude Code exposes a complete state-tracking surface: Notification fires
the moment Claude stops to ask for input, and Stop fires when the agent
finishes a turn. Codex's hook surface is narrower. There's no
Notification-equivalent — PermissionRequest fires only for explicit
permission asks, not the general "I'm waiting on you" pauses that reasoning
models (GPT-5, o-series) lean on. Result: a Codex pane can sit silent at a
> prompt with no hook ever firing.
termy fills the gap with a two-stage silence detector: ~8 s of PTY quiet
moves the chip to a silent POSSIBLY_WAITING (visual only, no sound), and
~12 s of further quiet promotes it to WAITING (chip + macOS notification).
Any byte from the PTY reverts the silent state instantly. The trade-off:
Codex WAIT lands ~20 s after the agent actually paused rather than
instantly, and the false-positive rate isn't zero. If Codex ships a
Notification-equivalent in future, termy will switch to it and drop the
heuristic.
| ⌘G | Jump to next WAIT pane |
| ⌘0 | Show all projects |
| ⌘1–9 | Jump to project filter N (hold ⌘ to see numbers) |
| ⌘⌥1–9 | Jump to dashboard chip N (hold ⌘⌥ to see numbers) |
| ⌘K | Fuzzy project switcher |
| ⌘[ / ⌘] | Previous / next pane |
| ⌘⌥←↑↓→ | Focus pane in direction |
| ⌘D / ⇧⌘D | Split row / column |
| ⌘⏎ | Toggle maximize focused pane |
| ⌘W | Close focused pane |
| ⌘/ | Full shortcut overlay |
Claude Code ──hook──▶ termy-hook (CLI) ──unix socket──▶ HookDaemon (in-app actor)
Codex CLI ──hook──▶ termy-hook --agent codex ──┘ │
│
┌────────┴────────┐
▼ ▼
PaneStateMachine fg-process watcher
│ (synthetic
▼ SessionStart/End)
MissionControlView + Notifier
- SwiftTerm renders the PTY and owns the child shell lifecycle.
termy-hookis a tiny Swift CLI that Claude Code invokes per hook event. It slims the payload and sends one JSON line over/tmp/termy-$UID.sock.HookDaemonis an actor that receives those events and runs them throughPaneStateMachine, the pure reducer covered by 74 unit tests.- Synthetic
PtyExitfills the gap Claude Code's hooks can't: hard crashes (Ctrl+C mid-response, SIGKILL) fire no hook, so termy emits a synthetic event on PTY EOF. Details indocs/project-overview.md.
The core loop is working end-to-end:
- SwiftTerm panes with OSC 7 cwd tracking, splits, and ⌘-click URL open
- Hook daemon +
termy-hookend-to-end - XCTest coverage over the state machine, fuzzy match, persistence, and filter layout
- Workspace autosave + restore with project-scoped pane grids
- Developer ID-signed, notarized DMG pipeline (
scripts/dist.sh)
See TODOS.md for deferred work — screen-scraping fallback for non-hook
agents, separate LaunchAgent daemon, user-editable hook config, per-project
settings UI.
docs/project-overview.md— architecture, runtime, hook system constraintsTODOS.md— scoped-out work with reasoningapps/termy/Sources/— the app (25 files)apps/termy-hook/Sources/— the hook helper CLIapps/termy-tests/Sources/— 74 tests across 8 suites