Getting started
Terminal Deck does not install or authenticate an agent for you. Before it is of any use, at least one of Claude Code, Codex CLI or Gemini CLI has to be installed on your machine and already signed in — the sign-in happens in the CLI, in a terminal, exactly as it would without this app.
On first launch it checks what is there. If nothing usable is found you get a setup screen instead of an empty window; if something is, you go straight to the app. You can re-run that check any time from Settings → Setup, which prints the literal command it ran alongside what it concluded.
Open a project
⌘O picks a folder. Opening one starts a session in it straight away, so you are looking at a running agent rather than a configuration screen. Projects you have opened are remembered and listed in the Projects panel.
Start more sessions
⌘T starts another session in the current project. ⌘⇧T opens the new-session dialog instead, where you choose the agent, the profile it runs as, and whether it continues the last conversation in that folder rather than starting fresh.
Continuing runs the CLI’s own resume flag — claude --continue for
Claude Code, codex resume --last for Codex. Gemini has no confirmed
resume flag, so a Gemini session starts fresh rather than being handed a guess.
The layout
One strip of tabs in the window header holds your sessions and any browser tabs. A narrow rail down the left side switches the side panel between Projects, Overview, Task board, Files, Search sessions, Source control, GitHub, AI readiness, Alerts, MCP servers and Hooks. ⌘B collapses the panel when you want the whole window for the terminal.
Sessions
A session is the real agent CLI, running as a subprocess on a pseudo-terminal. The main process owns it; the interface only addresses it by id. Anything your CLI does in your own terminal, it does here.
The status dot
Every session tab carries a dot with one of four meanings. It is classified in the main process from a headless terminal emulator fed by the session’s output, so it is accurate for tabs you are not looking at, and it survives a CLI repainting its screen.
- Working
- Waiting at its prompt
- Needs an answer
- Idle or exited
Why not read the output stream directly? Because agent CLIs are full-screen applications that repaint by moving the cursor, so the last bytes written have no relationship to the bottom of the screen. Terminal Deck therefore renders the session into an off-screen emulator and reads that.
Scrollback
Switching tabs does not kill anything. The main process keeps a rolling buffer per session and replays it, so a tab you come back to shows what you left. The terminal also stays mounted underneath chat mode.
Finding your CLI
A GUI app on macOS inherits a minimal PATH and cannot see a
claude installed by nvm, Homebrew or into ~/.local/bin.
Terminal Deck asks your login shell for its real PATH once and spawns
every session with it. If a CLI works in your terminal, it works here.
Swarm view
⌘\ puts every running session into one grid. Cells are never narrower than about forty columns — the layout drops to fewer columns rather than showing a tidy grid of unreadable slivers.
Chat mode
Every session view has a Terminal / Chat toggle. Chat mode shows the same session as a conversation: what you asked, what the agent said, and nothing else. Tool calls, diffs and spinners stay in the terminal.
The source is Claude Code’s own JSONL transcript, under
~/.claude/projects/ — not the terminal output. The newest transcript
for the project folder is the live one, and it is polled every couple of seconds
while you are watching it.
The composer writes into the session’s terminal, so a message typed in chat mode is the same message the terminal view shows. Enter sends; Shift+Enter starts a new line.
Claude Code only. Codex and Gemini sessions run normally, but they do not write the transcript this view reads, so chat mode has nothing to show for them.
Replies are rendered as markdown with two deliberate restrictions: a fenced code block collapses to a one-line summary you can expand, because a 200-line diff otherwise buries the sentence explaining it; and links keep their text but lose their destination, which moves to the tooltip. This is an application window, and a one-click navigation out of it is not worth having in a reading pane.
Browser
Open a browser tab from the new-tab control in the header strip. It shares the strip with your sessions, so the page and the agent building it are one click apart.
Element inspect
Turn on inspect and click something on the page. Terminal Deck builds a CSS selector
for it, preferring a test-hook attribute — data-testid,
data-test-id, data-test, data-qa,
data-cy, data-automation-id — then a unique
id, then a positional path. That, the element’s visible label and the
page URL go into the focused session as context.
The page is treated as untrusted throughout. The guest side only reports facts about the clicked element; every judgement about what makes a safe selector, what needs escaping and what may reach a terminal is made in the main process against a closed list of attributes. The URL used is the one the main process knows, never the page’s own claim about itself.
Dev servers
The address bar offers the dev servers actually listening on this machine, named by the process holding each port, instead of guessing that you are on port 3000.
Devices, zoom, screenshots and recording
Device presets and zoom are there for checking a layout at another size. A screenshot of the tab is one click. The flow recorder turns what you did on the page into a list of steps you can hand to the agent; it only records while recording is explicitly on, and every step goes through the same sanitising layer as the inspector.
Logins and isolation
The browser keeps a persistent session, so a login survives a restart. Tabs can be isolated from one another when you need two accounts at once. If a dev server sits behind a sign-in you already have in Chrome, Settings → Browser can import those cookies — macOS will prompt for keychain access by name, and the cookie values never leave the main process. The same section clears cookies, storage and cache.
Git
The Source control panel shows the current branch and four lists kept apart because they mean different things: staged, unstaged, untracked and conflicted. Each entry carries the letter git printed for it, and its insertions and deletions.
Selecting a file shows its unified diff. Untracked files are diffed against
/dev/null so a new file still has something to show, and a configured
external difftool is bypassed rather than being launched at you.
A watcher keeps the panel current while an agent works, which is the point — it is how you notice a session has rewritten more than you expected.
Files
A tree of the project that loads one level at a time, and a viewer with syntax highlighting. ⌘P is quick open — fuzzy search across the project’s files.
- Files over 2 MB are refused with a note rather than loaded, because loading them freezes the window.
- A directory with more than 2,000 entries is truncated and says so. A generated folder can hold hundreds of thousands of files; a tree cannot.
-
node_modulesand.gitare always hidden, whatever your ignore files say. - A symlink that leaves the project root, loops back into its own ancestry, or points at nothing is marked as refused instead of being followed.
-
A
.deckignorefile in the project is read by the tree, layered over.gitignoreand evaluated last-match-wins, so it can re-include something git hides. It is the tree only for now — quick open enumerates throughgit ls-filesand a fixed list of build directories, and no watcher consults it.
Session search
⌘⇧F opens full-text search across past Claude Code transcripts — this project, or every project you have ever run one in. Results are ranked, with the match highlighted in a snippet of its surroundings, and each hit says which role it came from: your prompt, the assistant, thinking, a tool result or a system line.
It reads the transcript lines the cost reader deliberately throws away. That reader keeps only lines carrying a usage block, which on a real 15,000-line transcript discards every prompt you typed — precisely the text a search exists to find.
Transcripts get large, so files are streamed in chunks with the main process free between them, and a search can be cancelled while it runs.
GitHub
The GitHub panel runs your own gh CLI: open pull requests, open issues,
and how many notifications the repository has waiting. It uses the same remote
gh itself would pick, so this panel and gh pr list never
disagree about which repository you are looking at.
Failures are kept apart rather than collapsed into “something went wrong”, because
each has a different fix: gh not installed, not authenticated, an
expired token, a missing scope, not a git repository, no remote, no GitHub remote,
repository not found, no access, rate limited, network down, timed out. Each one
names the command that fixes it, with the raw output behind a disclosure.
Comment bodies are deliberately not fetched — asking for them turns a twenty-row list into megabytes of text nobody asked to download.
MCP servers
The MCP inspector reads the configuration you already have rather than keeping a
second list of its own: user scope from ~/.claude.json, plus the
per-project servers and the approval gates in your Claude settings. A server you
added with claude mcp add shows up here without being re-entered.
Connect to a server to see its tools, resources and prompts. A tool can be called from a form generated out of its own JSON schema, with required arguments enforced before the call goes out.
Only stdio servers can be dialled from here. HTTP and SSE servers are
listed with a note saying so, rather than being hidden as though your config were
wrong. Every call has a wall-clock timeout: an MCP server is an arbitrary process,
and it can fail to spawn, spawn and never speak, or die halfway through a listing —
none of which may hang the app.
Hooks
Provider hooks let the app know what a session is doing from the CLI itself rather than by watching its screen. The Hooks panel installs them into each provider’s real settings file:
| Provider | File | Events |
|---|---|---|
| Claude Code | ~/.claude/settings.json |
SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, Notification, Stop, StopFailure, SessionEnd |
| Codex CLI | ~/.codex/hooks.json |
SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop |
| Gemini CLI | ~/.gemini/settings.json |
SessionStart, BeforeAgent, BeforeTool, AfterTool, AfterAgent, Notification, SessionEnd |
Every entry written ends with the comment # terminaldeck-hook, and that
marker is the only thing that identifies an entry as ours. Nothing is removed for
looking like ours or pointing at our port — a machine that also runs another agent
workspace has that tool’s hooks in the same file, and eating them would break a
working install of somebody else’s product.
Writes are atomic and the file is copied to a backup before it is first touched. A
settings file that cannot be parsed as strict JSON is left completely alone: a
config the app does not understand is a config it must not rewrite. File modes are
carried across, so a private 0600 settings file is not quietly widened.
Hooks report to a small HTTP endpoint bound to 127.0.0.1 only, carrying
a token generated fresh on every launch and never written to disk by the app. That
stops confused software and drive-by browser requests. It is not a defence against
something already running as you and reading your home directory, and the app does
not pretend otherwise.
AI readiness
A weighted score for how well a project is set up for an agent to work in it, from ten independent checks:
- No secrets committed
- A CLAUDE.md
- A README
- A test script, a typecheck script and a lint script
- A .gitignore
- A git repository, and a clean working tree
- A lockfile
The score lands in one of four bands — strong from 85, fair from 65, weak from 40,
at risk below that. The secrets check is a gate rather than a heavy weight: a
project that has committed a .env is capped below every band however
good the rest of it is. A tidy repository that leaks its API keys is not “87%
ready”.
Some checks offer a fix — creating a CLAUDE.md, a README or a .gitignore, or adding a missing script. A scan never applies one. You see what the fix will do and which files it touches, and it runs only when you ask for it by name.
Alerts
The handful of things about a project worth interrupting for: context bloat, a session about to hit it, a session blocked on a question, an unusually expensive session, an agent CLI that is missing, a working tree left dirty. Each alert names the thing to do about it.
Every rule requires positive evidence — sessions that actually ran, a provider actually in use, enough priced sessions for a median to mean anything — so a brand-new project is silent. A panel that greets an empty folder with five warnings is ignored within a day, and then the one real alert is ignored too.
Context thresholds come from the same module the session inspector uses, so the meter and the alert can never disagree.
Overview and board
Overview is a per-project dashboard of drag-and-drop widgets: Sessions, Cost, Git, Board, AI Readiness and GitHub. The layout is saved per project.
Task board is a three-column board — To do, Doing, Done — with drag and drop, search and tag filtering. A card can start or resume a session for the task it describes, and moves to Done when that session completes.
Profiles
A profile is a separate agent login. Claude Code keeps everything about who you are
inside one config directory, and CLAUDE_CONFIG_DIR moves that
directory — so two sessions pointed at two directories are two different accounts,
with separate history and separate transcripts.
Choose a profile per session in the new-session dialog, set a default per project, or set one globally in Settings → Profiles.
Three things about how this behaves are worth knowing:
- Credentials live in the macOS Keychain, under a service name derived from the config directory. Signing into a second profile cannot overwrite the first one’s token — and deleting a profile’s directory does not log it out.
-
Your existing
~/.claudelogin appears as a profile the app will not rename, move or delete. -
Sessions running as that system profile spawn with
CLAUDE_CONFIG_DIRunset rather than set to its own path. Setting it to the default path is not a no-op: the CLI would then look for~/.claude/.claude.json, while a default install keeps its config one level up, and your normal login would look unconfigured.
Settings
⌘, opens the settings window: a list of sections on the left, one section on the right, and no OK button anywhere. Every change is written as you make it, so pressing Escape can never lose anything. If a write fails the footer says so rather than silently reverting the control under your finger.
| Section | What is in it |
|---|---|
| General | How sessions behave day to day. |
| Appearance | Theme, density and the terminal typeface. |
| Notifications | How a banner is delivered, with a test button. |
| Agents | What is installed, and which login a session runs as. |
| Setup | What the app needs on your machine, and what it found. |
| Browser | The built-in browser tab, its cookies and what it remembers. |
| Shortcuts | Every key the app answers to. |
| Profiles | Separate agent logins, side by side. |
| Advanced | Launch behaviour, diagnostics, files on disk, starting over. |
| Help | How this works, and what to do when it does not. |
| About | Version, licence and updates. |
The About section reports that this build has no update feed attached, rather than spinning and telling you that you are up to date. Nothing in this app checks for updates by itself.
Keyboard shortcuts
These are the chords the app dispatches today, on macOS. Everything else reaches the agent — CtrlC interrupts it and Esc stops what it is doing, exactly as they would in your own terminal, because neither is claimed by anything below.
Control is currently treated as a second ⌘. The window's key
handler tests metaKey || ctrlKey, so the base key of every
chord in this table is also taken when it is pressed with Control —
CtrlW, CtrlK,
CtrlP and CtrlB among them, which are
readline and tmux keys you may well want in the session. Only the keys listed
here are affected: CtrlC, CtrlD,
CtrlA, CtrlE,
CtrlR and Esc pass straight through. This is a
bug in the app, recorded here rather than papered over.
| Keys | What it does |
|---|---|
| ⌘O | Open a project |
| ⌘T | New session in the current project |
| ⌘⇧T | New session dialog — agent, profile, and whether to continue |
| ⌘W | Close the active tab |
| ⌘1 – ⌘9 | Jump to a tab |
| ⌘K | Command palette |
| ⌘⇧P | Command palette |
| ⌘P | Quick open a file |
| ⌘B | Collapse or show the side panel |
| ⌘\ | Swarm view |
| ⌘⇧F | Search past sessions |
| ⌘⇧I | Session inspector |
| ⌘, | Settings |
| ⌘/ | The shortcut sheet |
| CtrlC | Interrupt the agent — passed straight through, never intercepted |
| Esc | Stop what the agent is doing — passed straight through |
| Esc | Close the open dialog, when one is open |
The in-app sheet lists more than this. The app’s keymap file declares chords the app does not dispatch — find and clear in the terminal (⌘F, ⌘⇧K), copy the selection (⌘⇧C), next and previous session (Ctrl⇥, Ctrl⇧⇥), the second quick-open chord (⌘⇧O), and direct chords for the board, the dashboard, the file tree and the git panel. Pressing ⌘/ shows them because the sheet renders the whole table. Use the rail or the command palette for those panels until the chords are wired.
Three of them — ⌘D, ⌘⇧S and ⌘⇧W — are worse than unwired: they name split panes, and the split view they would open is not rendered anywhere in the app. Wiring the chord would not produce a split.
The menu bar and the command palette (⌘K) dispatch the same command ids, so most menu items and their shortcuts stay in step. Three qualifications, because “cannot drift” was too strong:
-
The menu bar’s Settings… item sends a command id
(
app.preferences) that nothing in the interface handles, so clicking it does nothing. The window’s own key listener has a separate case for ⌘,. - ⌘W and ⌘B are in the menu bar but not the palette, and ⌘1–9 is in neither.
- The palette lists ⌘⇧T beside “Continue last session”. The command does continue the last session; the keystroke opens the new-session dialog, where continuing is one of the choices.