# Terminal Deck > Terminal Deck is a free, MIT-licensed desktop application for macOS and Windows that runs AI coding-agent sessions — Claude Code, Codex CLI, Gemini CLI, or any agent you add by naming its command — in a single window. Each session gets a tab with a live status dot classified from what is drawn on that session's terminal screen, and token use, cache hit rate and context-window pressure are read from Claude Code's own JSONL transcripts. The app shows no prices at all — that was removed deliberately, because an API figure misleads a subscription user and a subscription figure cannot be computed from anything any provider publishes. It has a copilot for the app itself: an assistant that can see your sessions, read their transcripts, start new ones and change settings, built as a real session inside the same sandbox as any guest, with routines that fire on events rather than on a clock. Its embedded browser can be driven by an agent through the browser's own debugging protocol, and hands the page back the instant a human touches it. A paired phone, or any browser at app.terminaldeck.dev, can attach to a running session or start one, over a channel sealed end to end. It is at version 0.15.0. Two desktop platforms are published: a .dmg and .zip for macOS on Apple silicon, signed with an Apple Developer ID certificate but not notarised, with no Intel build; and an installer and a portable .exe for Windows on x64, not signed at all, with no ARM build. There is no Linux window, but the same core runs as a headless host — plain Node, no Electron — which is what makes a Linux server or a WSL distribution a machine a phone or desktop can open a session on. It installs with one line, `curl -fsSL https://terminaldeck.dev/install.sh | sh`, from the `terminaldeck` package on npm, and it has been left running in a WSL distribution under a user service holding real agent sessions. The Windows build is produced natively in CI and has been run on a real Windows 11 x64 machine: it installs, launches, opens projects and starts sessions, agent and plain shell, and remote access works there. The whole test suite runs on a Windows runner in CI and gates the installer. The localhost tunnel and in-place updating both work there: the tunnel dials whichever loopback the port is actually on, since Windows resolves localhost to ::1 first and most dev servers there bind IPv6 alone, and an installed build takes itself from one release to the next through its own panel and restarts, silently. The portable .exe is the exception and says so, because installing is the one thing a portable app does not do. It has no account, no telemetry and no analytics. This file is every page of https://terminaldeck.dev as one markdown document, generated from the pages themselves by scripts/build-llms.mjs. Built by Asad Iqbal. MIT licensed. Repository: https://github.com/asadev/terminaldeck --- # Home Source: https://terminaldeck.dev/ ## One workspace for every coding agent. Run Claude Code, Codex and Gemini side by side — and keep them running from your phone. free · open source · no account needed A request, ‘ship the release notes’, is typed into Terminal Deck and split between three agents on three machines: Claude Code on this Mac, Codex on a Windows PC and Gemini on a server, each signed in to a different account. They work at the same time and their results land in a shared release-notes list. The person walks away and the same live sessions appear on their phone. Codex stops to ask whether to bump the version; the phone approves it and Codex carries on. A skill called Systematic debugging, published by @obra, arrives from Commons and Claude Code picks it up. The last result lands and the release notes are ready. ### The tab strip Sessions and browser tabs share one strip in the window header. The dot is the whole point of it: at a glance, one session is mid-thought, one has finished and is sitting at its prompt, and one is blocked on a question you have not answered. A drawing of the strip, not a screenshot. The four states below are the ones the app classifies. - Working - Waiting at its prompt - Needs an answer - Idle or exited ### Sessions Every session is a real process in a real terminal. Nothing is emulated, wrapped or proxied. #### Status is read from the screen, not the output stream Agent CLIs are full-screen applications. They repaint by moving the cursor, so the last bytes a process wrote have no relationship to what is at the bottom of the screen — which is why guessing status from the output tail gets it wrong. Each session therefore feeds a headless terminal emulator in the main process, and the status comes from that rendered viewport. It works for tabs you are not looking at, and it survives the CLI redrawing itself. Claude Code’s prompt is matched as the `❯` it actually draws, across a window of lines rather than at the very end, because the prompt is never the last line on screen. #### Tabs keep their scrollback Switching away from a session does not tear it down. The main process holds the scrollback and replays it, so coming back to a tab shows the conversation you left, not an empty screen. #### You choose what a session is before it starts The new-session dialog picks the agent, the profile it runs as, and whether it continues the last conversation in that folder. What is shown as selected is what will actually be spawned, including when it was chosen by a remembered default — the dialog resolves the decision rather than displaying a guess. The list of agents is not four names compiled into the app. Name a command and it runs, with the caveat drawn on its own row rather than left for you to discover: an agent nobody here has characterised gets a terminal and not a model picker. The command is resolved against your login shell’s `PATH` before it is saved, so a name that cannot be found is refused at the form rather than becoming a row that dies when you pick it. #### The controls are in the chrome, beside the session Model, effort, connectors and usage sit beside the account, above the session. Clicking a model changes the session that is running — and refuses, saying why, when you have something typed in the prompt, because the way a CLI is told to change model is a line typed at it and the next Return would have been yours. #### It finds your CLI the way your shell does A GUI app on macOS inherits a minimal `PATH` and genuinely cannot see a `claude` installed by nvm, Homebrew or into `~/.local/bin`. Terminal Deck asks your login shell for its real `PATH` once, and every session spawns with it. ### A copilot for the deck itself An assistant pinned above the session list that can see your sessions, read their transcripts, start new ones and change settings — a developer’s assistant, not a general one. No inbox, no calendar. #### It is a real session, and you can read everything it reads It is not a hidden service with a chat box in front of it. Its folder, its instructions, its memory and its transcript are ordinary files, and Settings → Copilot shows you exactly what it is handed before it starts. It runs inside the same sandbox as any session started from another device — including from us. That was measured against `sandbox-exec` rather than asserted: it cannot read your home directory, your SSH keys, your keychain or another account’s login. One consequence is worth stating rather than hiding, and the app states it on the copilot’s first screen: because the keychain is closed to it, it starts signed out. Its routines and its action log live outside the one folder it can write to. A routine it could write for itself without asking is not gated, and an audit log the audited party can edit is not an audit log. #### Routines run when something happens, not when a clock says so Saved instructions with a trigger: a session finishing, a session failing, git state changing, a file changing. A schedule is one trigger among several rather than the foundation. A routine that fires while nobody is at the machine and then needs consent for something is refused immediately, and says so, rather than hanging on a dialog no one is there to answer. ### Usage and context Read from the same transcripts. No scraping of terminal output, no estimate from character counts — and no prices, for a reason worth reading. #### Tokens, cache and the context window Per session: tokens split into fresh input, output, cache read and cache write; the share of the prompt that came from cache, which in a long agent session is most of it; how many deduplicated requests it took; and how full the context window is, with a warning when it bloats and a count of the compactions so far. Per project: what today has moved, across every session. The transcript is tailed incrementally rather than re-read, so a live session costs a stat call plus the bytes that actually arrived. #### Why there is no dollar figure There used to be one, and it was removed. The API price was real arithmetic and it still misled: almost everybody running this is on a subscription, and telling someone on a flat monthly fee that a session cost four figures states a number that never left their account. The subscription figure cannot be computed at all — no plan publishes a token allowance or a per-token value, and the multiplier in “Max 5×” is a multiple of an allowance, not of a price. One number was misleading and the other was invented, so the app counts tokens and says nothing about money. #### The session inspector ⌘⇧I opens one session in detail: a timeline of requests, where the tokens went, a breakdown by model, the largest requests, which tools were called and which of them kept failing, and how the context window filled up over time. There is a context meter with a bloat warning, and the alerts panel reads its thresholds from the same place so the two can never disagree. ### The browser tab A browser inside the window, so the thing the agent is building and the agent itself are one click apart. #### Click an element, and the agent gets a selector Turn on inspect, click something on the page, and the app builds a CSS selector for it — preferring a test-hook attribute like `data-testid`, then a unique `id`, then a positional path — along with the element’s visible label and the URL. That goes into the focused session as context you did not have to type. The page is treated as hostile throughout. The guest side only reports facts about the clicked element; every decision about what is a safe selector, what gets escaped and what reaches a terminal is made in the main process, against a closed list of attributes. #### Dev servers, device sizes and a flow recorder It looks for the dev servers actually listening on this machine rather than guessing port 3000. Device presets and zoom are there for checking a layout, screenshots are one click, and a flow recorder turns what you did on the page into steps you can hand to the agent. Logins persist between launches, tabs can be isolated from each other, and if a dev server sits behind a Google sign-in you can import the cookies from your installed Chrome instead of signing in twice. #### An agent can drive the page, and hand it back Driving is done through the browser’s own debugging protocol with a real actionability loop — resolve the element, check it is visible, wait for its box to stop moving, hit-test the point, then dispatch. Screenshots are for showing you what happened, not for deciding what to click. There is one page and the two of you take turns on it. The moment you interact with the page yourself, the agent’s next command is refused — and while you hold it, every command is refused, reads included. That is deliberate and it is the whole reason the rule is not limited to clicks and typing: a screenshot taken while you are entering a password is the leak, and you cannot redact what was never produced. ### The panels beside the session Everything you would otherwise leave the window to check, in the side dock. #### Git Branch, and the four lists that matter — staged, unstaged, untracked, conflicted — with per-file insertions and deletions, a unified diff for any file, and a watcher that keeps it current while the agent works. #### Files A tree that loads a level at a time, a viewer that refuses files over 2 MB rather than freezing on them, and symlinks that leave the project refused outright. `.deckignore` is honoured by the tree, layered over `.gitignore` so it can re-include a file git hides. #### Artifacts Every file your agents wrote or changed in this project, with the diff of each change. It answers the question you actually have after leaving four sessions running — not what did they say, but what did they touch. #### Session search Full-text search across past Claude Code transcripts — this project or all of them — with ranked, highlighted snippets. It reads the lines the usage reader throws away, which is where your prompts and the tool results actually are. It lives in the command palette under ? rather than on a page of its own: a palette is where people look for search, and it is one keystroke from wherever you are. #### GitHub Open pull requests and issues through your own `gh` CLI. Every failure is a distinct state with the command that fixes it — “you are not signed in” never looks like “this folder has no GitHub remote”. There is no notification count: the endpoint that carries one accepts only a classic personal access token, which a GitHub App cannot issue, so the bell was removed rather than left showing a number it could not fetch. #### MCP servers It reads the MCP configuration you already have, at user, project and local scope, instead of keeping a second list that would drift. Connect to a stdio server, browse its tools, and call one through a form generated from its schema. #### Hooks Installs callbacks into Claude, Codex and Gemini’s real settings files, every entry tagged `terminaldeck-hook` so it is only ever ours that get removed. Writes are atomic and the file is backed up before it is first touched. #### AI readiness Ten weighted checks on whether a project is set up for an agent — CLAUDE.md, a README, test, typecheck and lint scripts, gitignore, a lockfile, a clean tree — and fixes you can apply one at a time, after reading what each will touch. Committed secrets cap the score outright rather than costing it a few points. #### Alerts Context bloat, a session blocked on a question, an unusually expensive session, a missing CLI, a tree left dirty. Every rule needs positive evidence, so a brand-new project is silent — an alerts panel that cries wolf on day one is ignored by day two. #### Overview A live board of the sessions running right now — what each agent is doing, how long it has been at it, and which one is waiting on you — plus a per-project grid of drag-and-drop widgets (sessions, usage, git, AI readiness, GitHub). No progress bars: an agent does not report progress, and an invented number is worse than none. There is no task board. One was built and then deleted, code and all: a board is a thing you keep up to date by hand, and nothing else in this app asks that of you. ### More than one of everything #### Accounts: separate logins that cannot mix An agent CLI keeps who-you-are inside one configuration directory, and an environment variable moves that directory. Point two sessions at two directories and they are two different logins, with separate history and separate transcripts. Pick an account per session, per project, or as your default. An account belongs to an agent, so adding one asks which. Which agents that works for was measured against the real CLIs rather than assumed, because the variable that moves an agent’s configuration is not always the one that moves its login. Claude Code and Codex CLI both isolate, and are offered. Gemini CLI is listed and refused with the reason on the row: its token sits in one keychain slot no directory moves, so a second sign-in would overwrite the first rather than sit beside it. Your existing login is shown as an account the app will not rename, move or delete. #### Swarm view ⌘\ puts every running session in one grid, sized so a cell is never narrower than about forty columns. A tidy grid of unreadable slivers is worth less than one tall column you can actually read, so the layout gives up on being square before it gives up on being legible. #### Split panes ⌘D puts two sessions side by side in the same window, with ⌘⌥← and ⌘⌥→ to move between panes, a draggable divider, and a button in the toolbar so it is not chord-only. #### Settings that save as you change them Eleven sections — General, Appearance, Notifications, Coding AI, Tools, Browser, Scraping, Copilot, Power, Advanced and Help, plus a Linux one on Windows for WSL — with no OK button anywhere. A value on screen is a value on disk, and if a write fails the footer says so instead of quietly reverting the control under your finger. ### Away from the desk The sessions stay on your computer. Getting a phone attached is six digits, not a network to set up. #### Six digits, then approve it The desktop shows a six-digit code; you type it into the phone. There is no QR code and no pairing link — the QR did not work, and a link is a live bearer secret with a trip through a messaging app attached to it, so both were deleted rather than hidden. The code lives sixty seconds, is single-use, and dies after five wrong guesses. Redeeming it does not pair anything on its own: it creates a pending device that a person has to approve on the machine itself. The same six digits pair one computer to another. #### They find each other by themselves Nothing is set up on your network: no port forwarding, no VPN, no account and nothing else to join. Both ends dial out and a rendezvous server puts them together — it carries sealed bytes it holds no key for, and it is the one piece of plumbing in this product. How that is built, and what it can and cannot see. #### A browser, an Android APK, and an iPhone build on TestFlight app.terminaldeck.dev is the web client: open it anywhere, type the six digits, and you have the session list and a real terminal. Nothing to install, and it follows your system's light or dark setting. Both phone clients are native, with a real terminal and a key bar for the characters a phone keyboard hides. Neither is in an app store: Android is a signed APK on the download page, and iPhone is on TestFlight, internal testing only. #### Your dev server, on the phone Tap a port and the phone reaches a dev server running on the desktop. It is a raw TCP byte pipe rather than an HTTP proxy, so WebSockets, hot reload, service workers and cookies survive untouched. Only loopback is dialable, only ports something is listening on right now, and only after a person taps one. Stuck? Someone here has already hit it. A Codex session on this Mac is stuck: a Commons skill called Systematic debugging will not load, and the session needs you. Its owner types a question — does a Commons listing work with Codex as well as Claude Code? — and posts it to the Commons discussions. Four people arrive: @mira-k, @dan-w, @lena-r and @tobi-w. Three of them reply, one only reads. @dan-w's reply, that you have to restart the session after installing it, is marked as the answer that worked and lifts to the top of the thread with him credited. The answer travels back to the stuck session, which restarts, loads the skill and runs to completion. ### How it works The honest version, because the architecture is the reason to trust it with a folder full of your work. #### It runs the CLIs, it does not reimplement them Each session is the real `claude`, `codex` or `gemini` binary, spawned as a subprocess on a pseudo-terminal with your login shell’s `PATH`. There is no proxy in front of it and no wrapper around it. Whatever your CLI does in your terminal, it does here. An Electron main process owns every process and every piece of filesystem access; a single preload bridge is the only route between the interface and any of it. #### It never handles your credentials There is no account, no sign-in and no API key field anywhere in this app. The agent signs you in itself, inside the terminal, exactly as it would if you had run it yourself — and its credentials stay wherever it puts them, which on macOS is the Keychain. One deliberate exception, stated plainly: if you ask it to, the browser tab can import cookies from an installed Chrome so a dev server behind a login does not need signing into twice. That is user-initiated, macOS prompts for keychain access by name, and cookie values are never sent to the interface. #### Your data stays where it already is Usage, context and search all read files the agent already wrote on your machine. There is no telemetry, no analytics and no account service. Two connections leave your machine on the app’s own account, and both are named here: a fetch of the public release feed, carrying no identifier, and an outbound link to the relay, held open from launch so a phone you have already paired has something to attach to. The second is a switch in the Remote panel, it carries ciphertext the relay holds no key for, and switching it off is remembered (what is stored, and what leaves). Beyond those it is whatever pages you type into the browser tab, and whatever your agent does on its own. #### What is not built yet Translations are not built. Neither phone client is in an app store: iPhone is on TestFlight, internal testing only, and Android is a signed APK you download yourself. Notarisation on macOS and a Windows signing certificate are still to come, which is what puts one warning in front of each first launch. Updating in place works on macOS and on Windows, through to the relaunch, on real machines. The portable `.exe` is the exception, and the app says so rather than offering an update it cannot install: installing is the one thing a portable app does not do. On macOS, Apple silicon only: there is no Intel build, because macOS 27 does not run on an Intel Mac at all. The Windows x64 build is produced natively in CI and the whole test suite runs on a Windows runner, so a red Windows test stops the release rather than being waved through. There is no Linux window, and Linux is not left out: the same core runs as a headless host with no Electron in the process, which is what makes a Linux server or a WSL distribution a machine your phone and your desktop can open a session on. It installs with one line, its whole interface is eight commands in the terminal it was installed from, and it has been left running in a WSL distribution under a user service holding real agent sessions. What it does not have on that machine is a window. ### What you need **Operating system** macOS 12 or newer on Apple silicon, or Windows 10 or newer on x64. The macOS build is signed but not notarised and the Windows build is unsigned, so each warns once on first launch. There is no Intel Mac build; Windows on ARM runs the x64 build under emulation rather than natively. **An agent** At least one of Claude Code, Codex CLI or Gemini CLI, installed and already signed in. Terminal Deck does not install or authenticate them for you. **Optional** `git` for the git panel, the `gh` CLI for the GitHub panel. Both are only needed by their own panel. **To build from source** Node 22 or newer. --- # Features Source: https://terminaldeck.dev/features ## Features Everything below is in the app today. What is not built yet has its own section at the bottom, and the roadmap keeps the full list. ### Sessions and status A project holds as many sessions as you want, each one a real process in its own terminal. Tabs live in the window header; scrollback is replayed when you switch back, so leaving a tab does not lose what happened while you were away. Sessions can be started with Claude Code, Codex CLI, Gemini CLI or a plain shell, chosen per session, per project or globally. Every tab carries a status dot. The classification does not read the output stream — agent CLIs repaint by moving the cursor, so the tail of the stream has no relationship to the bottom of the screen. Each session instead feeds a headless terminal emulator in the main process and the status is read from that emulator's viewport, which means background tabs are classified as accurately as the visible one. - Working - Waiting - Needs input - Exited Around that: ⌘⇧T (Ctrl+Shift+T on Windows) for the new-session dialog, where continuing a project's last conversation is one of the choices. A tab starts named for its project folder and renames itself to whatever the agent is working on as soon as its output says — three tabs on one repository stop all reading the same word. A tab you are not looking at carries a dot when it produces output, and loses it the moment you look. Desktop notifications and sounds when a session finishes are wired up and have been watched working — a real banner from the shipped build, confirmed against macOS's own notification store, not just a code path. The one thing nothing in the app can do for you is answer macOS's authorisation prompt: it arrives as a banner with an Options disclosure hiding Allow, and until that is clicked every notification is dropped silently. ### The copilot An assistant for the deck itself, pinned above the session list. It can see your sessions, read their transcripts, start new ones and change settings, and it keeps a memory of your conversation with it. A developer's assistant and not a general one — no inbox, no calendar. It is built as **a real session** rather than a hidden service, which is the whole point: its folder, its instructions, its memory and its transcript are files you can open, and Settings → Copilot shows you what it reads before it starts. There is a short setup the first time — what to call it, what it should call you, which folder it works in, which account it runs as — and every question is skippable. It runs inside the same sandbox as a session started from another device, including from us. That was proved against `sandbox-exec` rather than asserted: it cannot read your home directory, your SSH keys, your keychain or another account's login. The consequence is stated on its own first screen instead of being discovered — because the keychain is closed to it, it starts signed out. Its routines and its action log live outside the one folder it can write to, since a routine it could author unasked is not gated and an audit log the audited party can rewrite is not an audit log. **Routines** are saved instructions with a trigger: a session finishing, a session failing, git state changing, a file changing. A schedule is one trigger among several rather than the foundation. One that fires while nobody is at the machine, and then needs consent, is refused immediately and legibly rather than left hanging on a dialog no one can answer. ### Usage and context Claude Code writes JSONL transcripts under `~/.claude/projects/`. That is the source for all of this — nothing is scraped from terminal output. A watcher tails the file per session and the numbers follow: tokens split into fresh input, output, cache read and cache write; the share of the prompt that came from cache; how many deduplicated requests it took; the tokens this project has moved today; and how full the context window is, with a warning when it bloats and a count of how many times the session has been compacted. **There is no money anywhere in the app.** It was there and it was removed. The API figure was real arithmetic and still misled, because almost everyone running this is on a subscription and a flat monthly fee does not become a per-session dollar amount; and the subscription figure cannot be computed at all, because no plan publishes a token allowance or a per-token value to measure a session against. One number was misleading and the other was unknowable, so the app counts tokens and says nothing about spend. Plan limits are the one exception, and they are only ever quoted: when Claude Code's own output has said something about the plan's usage window, the app repeats it and says when it read it. When nothing has been seen, it says so rather than estimating. ### Inspector, search and alerts - Session inspector ⌘⇧I. A timeline of every request, where the tokens went, the share served from cache, a breakdown by model, the largest requests, tool-call and tool-failure counts, and a context meter with a chart of how the window filled. - Transcript search Full-text search across past transcripts — this project or every project — with ranked, highlighted snippets. It lives in the command palette under ?, one keystroke from whatever you were already doing, rather than on a page of its own. - Smart alerts Missing tools, context bloat, session health. Built around one rule: a brand-new project must be silent. ### The project workspace - Git panel Branch, dirty files and diffs, kept current by a watcher. - File tree and viewer Browse the project and read files with syntax highlighting. - Quick open and palette ⌘P for files, ⌘K for commands, both fuzzy-matched. - GitHub panel Open pull requests and open issues, through the `gh` CLI. "Not logged in" and "no GitHub remote" never look the same. There is no notification count — GitHub's notifications endpoints take only a classic personal access token, which no GitHub App can issue, so the bell was removed rather than left showing a number it could not fetch. - Artifacts Every file your agents wrote or changed in this project, with the diff of each change — what they touched, rather than what they said. - AI readiness score Does the project have an agent instructions file, a README, tests, lint, typecheck, a gitignore and a clean git state — with one-click fixes. Any of the usual instruction filenames counts; the app does not require the one belonging to a particular CLI. A committed `.env` caps the score below every band. - `.deckignore` One list of what the app should not look at, with gitignore semantics and last-match-wins, so it can re-include a file `.gitignore` hides. Read by the file tree today; quick open enumerates through `git ls-files` and does not consult it yet. ### Dashboard Each project gets a drag-and-drop widget grid — sessions, usage, git, GitHub and AI readiness. There is no Kanban board. One was built, with search, tag filtering and cards that could start a session, and it was deleted — code, page, store, widget, menu item and shortcut together. A board is a thing you keep up to date by hand, and nothing else in this app asks that of you. Overview is a live board of what is running right now: what each agent is doing, how long it has been doing it, and which one is waiting on you. Deliberately no progress bar — an agent does not report progress, and a number the app invented would be worse than none. ### Many sessions at once - Swarm view ⌘\ puts every running session in one grid, sized so each cell is still wide enough to read a prompt. - Split panes ⌘D splits the window so two sessions are on screen at once, with ⌘⌥← and ⌘⌥→ to move between panes and a draggable divider. There is a split button in the toolbar too, so it is not chord-only. - Accounts An account belongs to an agent, so adding one asks which. Each gets its own configuration directory, which makes it a separate login with separate history and transcripts. Set per session, per project or globally. Isolation is per agent because the mechanism is per agent, and each was measured against the real CLI rather than assumed. Claude Code and Codex CLI both isolate a login and are offered. Gemini CLI is listed and refused, with the reason on its own row: its token lives in a single keychain slot that no configuration directory moves, so a second sign-in would overwrite the first rather than sit beside it. A switch that silently shared one login would look like the feature working right up until somebody pushed from the wrong account, which is why it is a refusal on screen rather than a missing row. ### The embedded browser A browser tab lives in the same window as the sessions. Load a page, turn on inspect, click an element, and the app hands the agent a CSS selector, a label and one line of context. The guest page is untrusted, so it only reports facts about the clicked element — tag, id, whether that id is unique, position among its siblings. Every judgement about which selector wins and what is safe to paste into a terminal happens on the trusted side, where it is tested against hostile input rather than hoped about. The browser also carries a device bar that resizes the real Chromium view rather than scaling a screenshot, so a 390px phone frame is a 390px viewport and media queries actually fire; a step recorder that turns what you did into a readable flow you can send to the agent; a drawing layer, so you can circle the thing you mean and send that; per-tab session isolation; and an import of your existing Chrome configuration. **An agent can drive that page.** Not by screenshot — through the browser's own debugging protocol, with a real actionability loop: resolve the element, check it is visible, wait for its box to stop moving, hit-test the point, then dispatch. Screenshots exist to show you what happened, not to decide what to click. There is one page and the two of you take turns on it. Any real interaction of yours takes the page back immediately, and while you are holding it every command from the agent is refused — reads included. That is not over-caution: a screenshot taken while you are typing a password is the leak, and nothing can redact what was never produced. It is also why the app does not need to run a second browser for the moments a human has to log in. ### From your phone or a browser The desktop puts a **six-digit code** on screen. Type it into the phone, approve the device on the desktop, and it can attach to a session that is already running or start a new one. There is no QR code and no pairing link: the QR did not work, and a link is a live bearer secret with a trip through a messaging app attached to it, so both were deleted rather than hidden. Six digits need no camera, no URL handler and no clipboard. The code lives sixty seconds, is single-use, and dies after five wrong guesses. Redeeming one does not pair anything on its own — it creates a pending device that a person has to approve on the machine itself. The same six digits pair one computer to another. The two machines find each other by themselves: nothing is set up on your network, no port is forwarded and there is no VPN to join. Both ends dial out and a rendezvous server puts them together; it carries sealed bytes it holds no key for, and Security is where that is explained and where it should be read. - Several machines at once One phone can hold more than one computer. Each gets its own transport, its own sealed channel against that machine's static key and its own sessions, so two machines cannot read each other's work — and all of them stay connected, so switching between them needs no handshake. - Your desktop's localhost Tap a port and the phone reaches a dev server running on the desktop. It is a raw TCP byte pipe rather than an HTTP proxy, so WebSockets, hot reload, service workers and cookies survive untouched. Only loopback is dialable, only ports something is listening on right now, and only after a person taps one. Windows works too: `localhost` resolves to `::1` there and dev servers commonly bind IPv6 only, so the tunnel settles which loopback the port actually lives on by dialling it once, rather than assuming `127.0.0.1`. - Tap an element, tell the agent The desktop browser's inspect mode, on the phone: tap something on the tunnelled page, say what should change, and it arrives in a terminal on the desktop as exactly one line. ### The clients All of them reach the desktop the same way and none runs a shell of its own: the processes stay on your computer and the client is a window onto them. - The web client Open app.terminaldeck.dev in any browser, type the six digits, and you have the session list and a real terminal. Nothing to install; it is a PWA, so it can be added to a home screen. It follows your system's light or dark setting. It lives on its own hostname rather than a path on this site, because a browser isolates stored credentials by origin and this one holds a pairing credential and a private key. On a borrowed computer, “just for this visit” is the default on the pairing screen. - iPhone — on TestFlight A native client with a real VT100 emulator, a key bar and key grid for the characters a phone keyboard hides, one-finger scrolling with momentum, long-press selection and copy, and automatic reconnect. **It is not on the App Store.** It is on TestFlight, internal testing only, and no submission has been made, so there is no link to give you. Ask for a place on the test in the community. - Android — an APK, not Google Play A Kotlin client, signed and on the download page. It is not on Google Play, so Android asks you once to allow installing it from your browser, and it cannot update itself — a new version means downloading it again. ### Integrations - MCP inspector Reads your own Claude Code MCP configuration rather than keeping a second list, connects to servers, and lists and calls their tools from a generated form. - Provider hooks Hooks installed into each CLI's real settings file, tagged `terminaldeck-hook`. Nothing is ever removed for merely looking like ours — a machine running another tool keeps its own hooks. - Settings window Eleven sections: General, Appearance, Notifications, Coding AI, Tools, Browser, Scraping, Copilot, Power, Advanced and Help — twelve on Windows, which gains a Linux section for WSL. Themes apply live, terminals included. The shortcut reference is a popover off the rail rather than the longest pane in the window, and pairing lives in Remote in the sidebar, because pairing a device is something you do rather than something you configure once. - Updates Settings → Help checks the release feed, offers what it found, and on your say-so downloads it, verifies it and swaps the app. Nothing downloads or installs on its own. Run end to end through to the relaunch on both macOS and Windows — the Windows half was proved on a real PC, one release finding and installing the next. The one exception is the portable `.exe`, which cannot replace itself and says so. ### Not built yet Named here so nothing above has to be read hopefully: - Translations — the app is English only - **Notarisation, and Windows code signing.** The `.dmg` is signed with an Apple Developer ID certificate, but it is not notarised — the developer account is not provisioned for it — so macOS needs the Open Anyway step. The `.exe` is not signed at all and Windows needs the SmartScreen override. The download page has both sets of words. - **The iPhone app on the App Store.** It is on TestFlight, internal testing only. No submission has been made, and the honest reason is that a reviewer has no desktop to pair with. - **The Android client on Google Play.** The APK is signed and published on the releases page, and the download page offers it; there is no Play listing, so it cannot update itself. - **Voice dictation.** The app cannot transcribe — in this Electron there is no on-device model and the browser API starts and then emits nothing. The microphone beside Send is therefore off by default and hands over to the operating system's own dictation when you switch it on in Settings → Tools. Useful, but not what a microphone beside Send promises. - **A Linux window.** There is no desktop build for Linux. Linux is not left out — the headless host is plain Node with no Electron in the process, installs with one line and has been proven in a WSL distribution — but it has no window, only its eight commands. - **A Mac talking to a Windows PC.** Machine-to-machine pairing has been run end to end against the real relay, the real trust store and the real pairing desk — with both ends in one macOS process on loopback, which is where every seam that has broken before lives. Across the internet, between two operating systems, it is the same code and it has not been run. See the roadmap → --- # Store Source: https://terminaldeck.dev/store/ ## Commons Skills, MCP servers, hooks, routines and tools that people have built for Terminal Deck. Conversations about these Publish something Everything here was written by somebody else and is published under their own name at terminaldeck.dev/@handle. Terminal Deck lists them; it does not review them, endorse them, or sell them. A paid listing shows the price its publisher stated and sends you to the publisher — we never take the money. Loading the catalogue… Every listing as one plain page → Reading this with an assistant? The whole catalogue is plain text at /store/llms.txt, one line each — or /store/llms-full.txt in full, with the description each publisher wrote for AI readers. This page searches and filters the catalogue in your browser, so with JavaScript off there is nothing here to search, and the search box, the filters and the Sort menu are hidden rather than left as controls that cannot answer. **Every listing as one plain page** is the whole catalogue, needs no JavaScript at all, and is linked above. --- # Docs Source: https://terminaldeck.dev/docs/ ### 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 → Coding AI**, 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 Overview, Files, Artifacts and Source control, then GitHub, AI readiness, MCP servers and Hooks, with Remote — the phones and computers that can reach this machine — in the rail's foot. ⌘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. #### 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. ### The copilot An assistant for the deck itself, pinned above the session list. It can see your sessions, read their transcripts, start new ones and change settings, and it keeps a memory of your conversation with it. It is a developer’s assistant and not a general one — there is no inbox and no calendar. The first time you open it there is a short setup: what to call it, what it should call you, which folder it works in, and which account it runs as. Every question is skippable, and skipping the name is a real answer rather than a blank — it is told that you have not named it and not to pick one for itself. The answers are written into its own instructions file, which is the same file **Settings → Copilot** puts in an editable box, so renaming it by editing that sentence works exactly as well as running the questions again. Nothing is written into its working directory, because that can be a folder you already keep an assistant in. #### What it can reach It runs inside the same sandbox as a session started from another device, and that includes being sandboxed from us. It was measured against `sandbox-exec` rather than asserted: it cannot read your home directory, your SSH keys, your keychain or another account’s login. One consequence is worth knowing before you meet it, and it says so on its own first screen — because the keychain is closed to it, **it starts signed out**. Its routines and its action log live outside the one folder it can write to. A routine it could author without asking is not gated, and an audit log the audited party can rewrite is not an audit log. #### Routines Saved instructions that run on their own, triggered by something happening rather than by a clock: a session finishing, a session failing, git state changing, a file changing. A schedule is one trigger among several rather than the foundation. A routine that fires while nobody is at the machine and then needs consent for something is refused immediately, and says why, instead of hanging on a dialog no one is there to answer. ### 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. #### An agent driving the page An agent can drive that tab. Not through screenshots — through the browser’s own debugging protocol, with a real actionability loop: resolve the element, check it is visible, wait for its box to stop moving, hit-test the point, then dispatch. Screenshots are how it shows you what happened, not how it decides what to click. There is one page, and you and the agent take turns on it. Any real interaction of yours takes it back at once, and the agent’s next command is refused. While you are holding it, every command is refused, reads included — which is the point rather than an excess of caution. A screenshot taken while you type a password is the leak, and nothing can redact what was never produced. It is also why there is no second browser to switch to when a human has to sign in. You can draw on the page and send that, alongside inspect and record. #### 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_modules` and `.git` are 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 `.deckignore` file in the project is read by the tree, layered over `.gitignore` and evaluated last-match-wins, so it can re-include something git hides. It is the tree only for now — quick open enumerates through `git ls-files` and a fixed list of build directories, and no watcher consults it. ### Artifacts Every file your agents wrote or changed in this project, with the diff of each change. It is the answer to the question you actually have after leaving four sessions running — not what did they say, but what did they touch. It sits where Search used to be in the rail. A row called Search between Files and Source control promised to search files and did not, and its own results had never been clickable; searching past sessions moved into the command palette, which is where people look for search anyway. ### Session search ⌘⇧F searches 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 lives in the command palette, under the ? sigil, rather than on a page of its own; ⌘⇧F opens the palette already in that mode, so the old chord still lands on the thing it always meant. The sidebar row it used to open is gone: a row called Search sitting between Files and Source control promised to search files, and it did not. It reads the transcript lines the usage 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 and open issues. There is no notification count — the endpoints that carry one accept only a classic personal access token, a GitHub App user token is not one, and no permission can be added to change that, so the bell was removed rather than left showing a number it could never fetch. 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: 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, a session that moved far more tokens than this project's usual, 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 sessions with tokens on them 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 **Overview** is a live board of the sessions running right now — what each agent is doing, how long it has been at it, and which one is waiting on you — over a per-project grid of drag-and-drop widgets: Sessions, Usage, Git, AI Readiness and GitHub. The layout is saved per project. There are deliberately no progress bars, because an agent does not report progress. There is no task board. One was built — three columns, drag and drop, cards that could start a session — and it was removed, page, store, widget, menu item and shortcut together. A board is a thing you keep up to date by hand, and nothing else in this app asks that of you. ### Remote — your phone, and any browser **Remote** sits in the foot of the rail and lists the phones and computers that can reach this machine. It is in the rail rather than in Settings on purpose: pairing a device is something you do, repeatedly, standing at two keyboards — not something you configure once. #### Pairing is six digits Open Remote and the desktop shows a six-digit code. Type it into the client and approve the device here. There is no QR code and no pairing link — the QR did not work, and a link is a live bearer secret that has to travel through a messaging app to be useful, so both were deleted rather than hidden. The code lives sixty seconds, is single-use, and dies after five wrong guesses. Redeeming it does not pair anything on its own: it creates a pending device that somebody has to approve on this machine. The same six digits pair one computer to another, so a desktop can drive another desktop's sessions. #### How the client reaches you This machine dials out to Terminal Deck's own rendezvous relay at `relay.terminaldeck.dev` and holds the link open; the client dials out too, and the relay staples the two together. Nothing is port-forwarded, nothing listens for an inbound connection, and there is no VPN or other network for you to set up or know about. The relay is treated as hostile, because every byte passes through it. The two ends run a Noise IK handshake — the pattern WireGuard uses — and every payload is sealed under keys the relay never sees. It learns that a host is online and how much traffic moved; it cannot read a command, inject a keystroke, or sit in the middle of the handshake without failing to decrypt on the first frame. It holds no account and no database: a host's name maps to an open socket in memory only, and nothing about it survives a restart. The switch is in Remote, and turning it off is remembered. #### The three clients #### What a client can do Attach to a session that is already running, with its scrollback replayed, or start a new one; hold several machines at once, each with its own sealed channel; and tap a port to reach a dev server running on the desktop. That last one is a raw TCP byte pipe rather than an HTTP proxy, so WebSockets, hot reload, service workers and cookies survive untouched — only loopback is dialable, only ports something is listening on right now, and only after a person taps one. The machine that owns the files decides which folders each paired device may **start a session in**. Read that literally: it is organisation, not a sandbox. A shell that starts in a granted folder can `cd` anywhere your user account can reach. The boundary is pairing plus your approval, not the folder list. ### Accounts An account is a separate agent login, and it belongs to one agent — so adding one asks which. The mechanism is the same idea in each case: the CLI keeps everything about who you are inside one configuration directory, and an environment variable moves that directory, so two sessions pointed at two directories are two different logins with separate history and separate transcripts. Which agents that works for was measured against the real CLIs rather than assumed, because the variable that relocates an agent’s configuration is not always the one that relocates its login, and getting that wrong does not fail loudly — it produces two account names quietly sharing one credential. - **Claude Code** — `CLAUDE_CONFIG_DIR`. On macOS the credential is in the login Keychain under a service name derived from the directory, so a second sign-in cannot overwrite the first one’s token, and deleting a directory does not log it out. - **Codex CLI** — `CODEX_HOME`. The credential is a file inside that directory, so it moves with the directory by construction. - **Gemini CLI** — listed and refused, with the reason on the row. Its token lives in a single keychain slot that no configuration directory moves, so a second sign-in would overwrite the first rather than sit beside it. Choose an account per session in the new-session dialog, set a default per project, or set one globally in **Settings → Coding AI**. Your existing login appears as an account the app will not rename, move or delete, and sessions running as it spawn with the variable unset rather than set to its own path — setting it to the default path is not a no-op, because the CLI would then look one directory deeper than a default install keeps its configuration, 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. Several things used to be sections here and are not. **Shortcuts** is a popover off the rail's foot — it was the longest pane in the window and the one nobody scrolls twice, which is the shape of a reference rather than a settings screen. **Setup** and **Accounts** were folded into **Coding AI**, and **About** is now the card at the top of Help. Every link that used to open any of them still lands on the pane its contents went to, including the application menu's own About item. The Help section checks the release feed and offers what it finds; on your say-so it downloads the release, verifies it and swaps the app, through to the relaunch. Nothing downloads or installs on its own, and if this build cannot update itself — the portable Windows executable is the one that cannot — it says so rather than offering an update it cannot install. ### Keyboard shortcuts These are the chords the app dispatches today, on both platforms. 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. **On Windows, a focused terminal keeps the plain Ctrl chords.** CtrlW, CtrlK, CtrlP and the rest are readline and tmux keys, and a terminal that lost them would be a broken terminal. So while a session has focus the app only claims the chords carrying Shift or Alt — which is why the command palette answers to CtrlShiftP there, and to ⌘K on a Mac, where ⌘ belongs to the application and Control belongs to the terminal. Click away from the terminal and every chord in this table works on either platform. This note used to say that Control was treated as a second ⌘ because the key handler tested `metaKey || ctrlKey`. That was true and is not: the matcher compares every modifier exactly, so CtrlW on a Mac no longer closes a tab on its way to the shell. **The sheet and this table are the same list.** Pressing ⌘/ — Ctrl/ on Windows — renders the app's keymap, and a test fails the build if any chord in that keymap has nothing that answers it. This note used to say the opposite, and list a dozen chords the app declared and did not dispatch, including three that named a split view the app could not open. The split view is real now — ⌘D splits the window, ⌘⌥← and ⌘⌥→ move between panes — and the chords in the table are the ones that answer. The menu bar and the command palette (⌘K, or CtrlShiftP on Windows) dispatch the same command ids, so most menu items and their shortcuts stay in step. Two qualifications, because “cannot drift” was too strong: - ⌘W and ⌘B are in the menu bar but not the palette, and ⌘1–9 is in neither. - The palette writes down no chord of its own. Each row asks the keymap what its command is bound to on the platform the window is running on, so a Windows user reads CtrlT where a Mac user reads ⌘T, and a row the keymap does not bind prints nothing rather than something invented. It used to carry seventeen hand-typed ⌘ strings, one of which was beside the wrong command. --- # Help Source: https://terminaldeck.dev/faq.html ## Help 40 questions people actually arrive with, answered in two or three sentences each — including the ones with awkward answers. ### Getting started What it is, what it costs, and what your machine needs. #### What is Terminal Deck? One window that runs your coding agents as real terminal sessions — Claude Code, Codex CLI, Gemini CLI, or an agent you add yourself by naming the command it runs. Every session gets a tab and a live status dot, so you can see which one is working and which one is waiting on you without clicking through them. It is a desktop app for macOS and Windows, and the same sessions open on your phone or in a browser. #### What does it cost? Nothing. It is free and MIT-licensed, there is no account and no paid tier, and nothing in it is held back for a version you have to buy. You pay whatever your agent’s own provider charges you, exactly as you would running that CLI in your own terminal. #### Which computers does it run on? macOS 12 or newer on Apple silicon, and Windows 10 or newer on x64. There is no Intel Mac build, because macOS 27 dropped Intel Macs entirely; there is no Windows-on-ARM build either, though Windows on ARM runs the x64 one under emulation. There is no Linux window at all — see Is there a Linux version? #### Do I need Claude Code, Codex or Gemini installed already? Yes. At least one of them has to be on your machine and already signed in before Terminal Deck is of any use, and the sign-in happens in the CLI itself, in a terminal, exactly as it would without this app. Terminal Deck installs none of them and never asks you for a login or an API key. There is no field anywhere in it to type one into. #### Why does my computer warn me the first time I open it? On macOS the app is signed with an Apple Developer ID certificate but it is not notarised, so Gatekeeper refuses it once. Click **Done** — not Move to Trash — then open System Settings → Privacy & Security and click **Open Anyway**. Right-click → Open no longer works; macOS 15 removed it. On Windows the build is not signed at all, so SmartScreen puts up a blue panel and hides the button behind **More info** → **Run anyway**. If you would rather not take either on trust, every release lists the SHA-256 of every file, and a build you compile yourself carries no quarantine flag at all. The whole walkthrough is on the download page. ### Agents and accounts The three agents, your own, and running more than one login at once. #### Which agents can it run? Claude Code, Codex CLI and Gemini CLI. All three are offered wherever the app asks which agent to start, and you can set a default per project or one for the whole machine. A session is that agent’s own binary running as a subprocess on a pseudo-terminal, so anything your CLI does in your own terminal it does here. #### Can I add an agent that is not one of the three? Yes. You name the command it runs and it appears beside the others when you start a session. It gets a tab and a status dot like anything else, because the status is read from what the session draws on its own screen rather than from anything specific to one agent. #### Can I run two accounts for the same agent? For Claude Code and Codex, yes. An account is a separate configuration directory — `CLAUDE_CONFIG_DIR` and `CODEX_HOME` — so two sessions pointed at two directories are two different logins, with separate history and separate transcripts. Choose one per session, set a default per project, or set one globally. Gemini is listed and refused, with the reason on the row: its token lives in a single keychain slot that no configuration directory moves, so a second sign-in would overwrite the first rather than sit beside it. Your existing login shows up as an account the app will not rename, move or delete. #### Why is usage empty on my Codex or Gemini session? Token and context usage, the session inspector and session search all read Claude Code’s own JSONL transcripts under `~/.claude/projects/`. Codex and Gemini sessions run perfectly well — they simply do not write the file those panels read, so there is nothing for them to count. The terminal, the browser, the phone and everything else work the same for all three. #### Why does it never tell me what a session cost? Because no honest number is available. An API price misleads somebody on a subscription, and a subscription price cannot be computed from anything any provider publishes. So the app reports tokens, cache hit rate and how much of the context window is left, and no panel anywhere shows money. That was removed deliberately rather than never built. #### What is the copilot? An assistant for the app itself, pinned above the session list. It can see your sessions, read their transcripts, start new ones and change settings, and it keeps a memory of your conversation with it. It runs inside the same sandbox as a session started from another device: it cannot read your home directory, your SSH keys, your keychain or another account’s login. Because the keychain is closed to it, it starts signed out, and says so on its own first screen. Its routines run on something happening rather than on a clock — a session finishing, a session failing, git state changing, a file changing. ### Your phone and other machines Reaching a session from somewhere else, and what that costs you in trust. #### Can I use my sessions from my phone? Yes. A paired phone, or any browser at app.terminaldeck.dev, can attach to a session that is already running — with its scrollback replayed — or start a new one, and can hold several machines at once. The web client needs nothing installed and is available now. It is on its own hostname because a browser keeps stored credentials apart by origin. #### Is there an iPhone or an Android app? The iPhone app is on TestFlight, internal testing only. It is not on the App Store and no submission has been made. The Android client is a signed APK on the releases page — a real download, listed on the download page. It is not on Google Play, so Android asks you once to allow installing from your browser, and it cannot update itself. On an iPhone today the web client is the answer: app.terminaldeck.dev in Safari needs nothing installed and can be added to the home screen. #### Do I need a VPN, port forwarding or a static IP? No, and there is nothing for you to set up. Your machine dials out to Terminal Deck’s own rendezvous relay at `relay.terminaldeck.dev` and holds the link open, the client dials out too, and the relay staples the two together. Nothing is forwarded and nothing listens for an inbound connection. The switch is in the Remote panel in the sidebar, and turning it off is remembered. #### Can the relay read what I type? No. The two ends run a Noise IK handshake — the pattern WireGuard uses — and every payload is sealed under keys the relay never sees, so it cannot read a command, inject a keystroke, or sit in the middle of the handshake without failing to decrypt on the first frame. It learns that a machine is online and how many bytes moved. It holds no account and no database: a machine’s name maps to an open socket in memory only, and nothing about that survives a restart of the relay. To be exact about what is not claimed: the relay keeps no per-connection log, but it sits behind an ordinary reverse proxy on a rented server. The guarantee is about content, and it is structural — the keys are not there. #### How do I pair a phone or another computer? Open Remote on the machine and it shows six digits. Type them into the client, then approve the device on the machine — redeeming the code does not pair anything on its own, it creates a device waiting for your approval. The code lives sixty seconds, is single-use, and dies after five wrong guesses. There is no QR code and no pairing link; both were deleted rather than hidden, because a link is a live secret that has to travel through a messaging app to be useful. The same six digits pair one computer to another. That has been run end to end against the real relay with both ends in one macOS process — a Mac has never talked to a Windows PC. #### Can my phone open a dev server running on my desktop? Yes — the client lists the ports something is listening on, and you tap one. It is a raw TCP byte pipe rather than an HTTP proxy, so WebSockets, hot reload, service workers and cookies survive untouched. Only loopback is dialable, only ports that are live right now, and only after a person taps one. #### If I grant a device one folder, is it locked to that folder? No. Read the wording literally: the machine that owns the files decides which folders each paired device may start a session in. That is organisation, not a sandbox. A shell that starts in a granted folder can `cd` anywhere your user account can reach. The boundary is pairing plus your approval on the machine, not the folder list. ### Servers and the headless host Linux, a box with no screen, and what survives a restart. #### Is there a Linux version? There is no Linux window and no Linux desktop build. The same core does run as a headless host — plain Node, no Electron in the process — which is what makes a Linux server or a WSL distribution a machine your phone and your desktop can open a session on. #### What is the headless host? Terminal Deck without a window: a background process that joins the relay and serves sessions, driven from a phone or from Terminal Deck on another computer. Eight commands, no configuration file, and no Electron. It is what turns a box you have no screen on — a rented server, a spare machine, a WSL distribution — into one more machine in your deck. One line installs it: `curl -fsSL https://terminaldeck.dev/install.sh | sh`, which is on the download page with what it writes and how to undo it. #### Can I manage a server that has no screen? Yes, from its own page — on your phone or on the desktop. Restart, stop or start the host over the relay, and connect GitHub there, so an address that has gone offline is not a dead end. The copilot runs on a server too, with your phone as its screen, because the server has none of its own. #### Do my sessions survive closing the app? Nothing keeps running while the app is closed — a session is a real CLI process, and closing the app ends it. What survives is the conversation. Claude Code writes every turn to its own transcript, so a restored session is handed its resume flag and comes back continued rather than fresh, with the last part of the conversation painted back above its own output. That replay is a read, never a re-run: nothing is sent to the CLI, no command re-executes, and nothing is summarised. ### Commons and publishing The community catalogue, how to read a listing, and how to put one up. #### What is Commons? The community catalogue at terminaldeck.dev/store: skills, instructions, hooks, MCP servers, browser extensions, routines and open-source tools that other people published, each under their own name at `terminaldeck.dev/@handle`. Reading it needs no account and no JavaScript — the whole catalogue is on one plain page. Publishing and posting need an account, and sign-in is GitHub or Google. #### Does Terminal Deck sell any of this? No, and no money passes through this site. Terminal Deck lists these things; it does not endorse them and it never handles payment. A paid listing shows the price its publisher stated and one link to the publisher’s own page. You buy it there, from them, under their terms. A person does read every first version before it appears — that is a safety check on what it would do to somebody’s machine, not a recommendation. #### How do I judge a listing before I install it? Start with the tier, which is the short answer to how much of your machine it can touch. Tier 1 is text only and nothing runs; tier 2 ships scripts your agent may run; tier 3 runs a program on this machine. The needs line under it says whether it also wants Node, Python, an account somewhere, a key you supply, or another app installed. Every listing is pinned to one commit and a sha256, never a branch, so what a person reviewed is exactly what you install — and the shelf does not move when a new version ships upstream until that version has been read too. #### Why does a listing show no rating or install count? Because there is not enough behind it yet to mean anything. An install count appears only once a listing passes ten, and a rating average only once it passes five votes. Below that the page shows nothing at all rather than a number that would mislead you. A store printing five stars off a single vote is lying. #### How do I publish something? Paste the address of your public repository on the publish page. It is read and you are shown exactly what would be listed before anything is created. The repository can be on GitHub, GitLab or Codeberg, but signing in to publish is GitHub, because only GitHub can confirm you can write to the repository you are listing. Your publisher name goes into every address your work is published at and cannot be changed afterwards, so choose it deliberately. #### What happens after I press Publish? The listing is created as a draft and its first version goes into a queue. A person reads the files, the manifest and what it would do to somebody’s machine, then approves it, refuses it with a reason you will see, or bans the account. Nothing appears in the store until that has happened. Afterwards, updates that do not change the risky parts publish themselves. Nothing is emailed to you — Commons has no mailer. Your account page is where it says what happened. #### Can I delete my Commons account? Yes, from your account page, and it cannot be undone. Your listings come off the store, your name, picture, bio and links are erased, and your sign-in is deleted. Your handle stays reserved so nobody else can publish under it. What you wrote in discussions stays, so other people’s threads still make sense, and copies of your work that people already installed stay on their own machines — nothing here can reach those. ### Privacy and security What is collected, what is written down, and what is deliberately not defended. #### What does the app collect about me? Nothing. There is no account, no analytics, no telemetry and no crash reporting, and the source contains no analytics SDK of any kind. It makes one network request of its own: it fetches the public release feed on GitHub to see whether a newer version exists. No identifier, no version history and no machine fingerprint goes with it, and nothing downloads or installs unless you press the button. It also holds a link open to the relay, so a phone you have already paired has something to attach to. That link says only that the machine is online, and the switch for it is in the Remote panel. #### Does Terminal Deck see my prompts or my code? No. A session is the real `claude`, `codex` or `gemini` binary running as a subprocess — there is no proxy, no wrapper and no interception, so when it contacts its provider it does exactly what it would do if you had typed the command yourself. Your prompts, your code and your files are handled under that provider’s policy, not ours. #### What does it write to my disk? One folder of its own: your settings, the projects you have opened, your agent accounts, dashboard layouts, the copilot’s files and its action log, your routines, the devices you paired and their keys, and a rotating application log capped at about 1.5 MB. The only thing outside that folder is a backup of an agent CLI’s own settings file, taken before hooks are first written into it. None of it is synchronised or uploaded, and deleting the folder deletes all of it. Settings → Advanced lists the real paths on your machine and opens them, which is more reliable than a path printed on a web page. #### Is the agent sandboxed from my files? No, and that is a decision rather than an oversight. Terminal Deck runs the agents with your permissions and does not confine, filter or review what they do — the same as running them in your own terminal. What is defended is everything around them: a hostile page loaded in the browser tab, the local endpoint agent hooks report to, secrets leaking through a log or a support bundle, and the relay that carries a remote session. #### Does this website track me? No. There is no analytics, no tag manager, no comment widget and no font CDN — the typefaces are served from this domain — so there is nothing here to consent to and you were not asked. If you sign in to Commons you get a session cookie, sealed and readable only by the server, and one plain flag cookie beside it whose only job is to decide which header the page draws. Signing out clears both. #### How do I report a security problem? Through the security page, rather than a public issue. That page is also where the threat model is written down: what is defended, what is deliberately not, and the known limit beside each defence. ### Trouble The four things that go wrong most, and where to go when it is a fifth. #### It says it cannot find my agent 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, so if a CLI works in your terminal it works here. If it still does not, Settings → Coding AI re-runs the check and prints the literal command it ran alongside what it concluded — so you get the command as well as the verdict. #### The GitHub panel says something is wrong. It runs your own `gh` CLI, so the fix is nearly always a `gh` command. The failures are kept apart rather than collapsed into “something went wrong” — not installed, not authenticated, an expired token, a missing scope, no remote, no access, rate limited — and each one names the command that fixes it, with the raw output behind a disclosure. There is no notification count in it, and there will not be: the endpoints that carry one accept only a classic personal access token, which no permission can be added to change. #### My MCP server is listed but will not connect. Only stdio servers can be dialled from here. An HTTP or SSE server is listed with a note saying so rather than hidden, so seeing it there does not mean your configuration is wrong. The panel reads the servers you already configured — user scope from `~/.claude.json`, plus your per-project ones — so a server added with `claude mcp add` appears without being re-entered. #### On Windows, some shortcuts do nothing while I am typing in a session. That is on purpose. `Ctrl` `W`, `Ctrl` `K` and `Ctrl` `P` are readline and tmux keys, and a terminal that lost them would be a broken terminal — so while a session has focus the app claims only the chords carrying Shift or Alt. The command palette is `Ctrl` `Shift` `P` there. Click away from the terminal and every chord works. On a Mac the question does not arise: `⌘` belongs to the app and `Control` belongs to the terminal. #### Where do I get help, or report a bug? Ask in the community — four topics: help, show and tell, ideas and problems. Reading it needs no account; posting needs one. For a bug, the issue tracker. Settings → Advanced makes a support bundle to paste into it: generated only when you ask for it, copied to your clipboard, and sent nowhere. Everything in a bundle is passed through redaction first and it reports how many substitutions it made — but redaction is software, so read one before you post it. ### Not here? Four places that go further than a paragraph can. - Docs — what every panel does, and the shortcuts the app really dispatches. - Features — everything in the app today, and a list of what is not. - Community — help, show and tell, ideas and problems. No account needed to read. - Issue tracker — for a bug, with a support bundle from Settings → Advanced. --- # Download Source: https://terminaldeck.dev/download ## Download Terminal Deck 0.15.0. Free, MIT-licensed, and there is no account to make. Pick the machine you want it on. Every build that exists is below, including the two for phones that are not in an app store yet — those say so instead of being left off the page. ### Choose a machine What each build is, who it is for, and what happens the first time you open it. Every download button resolves to the newest release as you click it, so none of them can quietly hand you an old one. - macOS A Mac with an Apple-silicon chip, on macOS 12 or newer. There is no Intel build: macOS 27 does not run on an Intel Mac at all. **First launch:** macOS says it cannot verify the developer and offers you Done or Move to Trash. Click **Done**, then open **System Settings → Privacy & Security** and click **Open Anyway** next to the message naming Terminal Deck. Once, and never again. The four steps in full. Download the .dmg `terminaldeck-0.15.0-arm64.dmg` 119 MB · signed, not notarised - Windows — installer A PC on Windows 10 or newer, x64. It installs for your account only, so it never asks for an administrator password. Windows on ARM runs this build under emulation; there is no native ARM one. **First launch:** a blue **Windows protected your PC** panel with only a Don’t run button. Click **More info**, then **Run anyway**. Why it warns. Download the installer `terminaldeck-0.15.0-x64-setup.exe` 107 MB · not signed - Windows — portable The same build as one file that runs without installing anything. Take this one for a machine you would rather not write to Program Files on, or to carry it on a stick. **The one thing it cannot do** is update itself — it is a loose file with nothing to replace it — so a new version means coming back here. Everywhere else, updating in place works from inside the app. Download the portable .exe `terminaldeck-0.15.0-x64-portable.exe` 107 MB · not signed - A server with no screen A Linux box, a rented server, or a WSL distribution on Windows — anything you reach over SSH. It is the same core with no window and no Electron in the process, so the sessions run there and your phone or your desktop opens them. It brings its own Node if the machine has none, writes nothing outside your home directory and never runs as root. Then `terminaldeck pair` prints six digits to type into a phone. `rm -rf ~/.terminaldeck` undoes all of it. `curl -fsSL https://terminaldeck.dev/install.sh | sh` `terminaldeck` on npm · reads the script first at terminaldeck.dev/install.sh - Android An Android phone or tablet. It is a client, not a copy of the app: the sessions stay on your computer and this is a window onto them. **It is not on Google Play.** This is the signed APK from the releases page, so Android will ask you to allow installing from your browser the first time, and it cannot update itself — a new version means downloading it again from here. Download the APK `terminaldeck-0.15.0.apk` 4 MB · signed by us, not by Google - iPhone and iPad A native client with a real terminal, a key bar for the characters a phone keyboard hides, and long-press selection. **There is nothing to download.** It is on TestFlight, internal testing only — not on the App Store, and no submission has been made. There is no public link to give you, so there is no button here rather than one that goes nowhere. Ask for a place on the test in the community. **What works on an iPhone today:** app.terminaldeck.dev in Safari. Type the six digits and you have the session list and a real terminal, with nothing installed. It can be added to the home screen. - Any browser A borrowed computer, a work laptop, a tablet — anything with a browser and no permission to install software. It holds a pairing credential and a private key, so it lives on its own hostname rather than a path on this site: browsers keep stored credentials apart by origin. “Just for this visit” is the default on the pairing screen. Open the web client Nothing to install · works now Every checksum, every earlier build, and the macOS `.zip` the in-app updater fetches are on the releases page. Or build it yourself — the repository is the whole product, and a build you made carries no quarantine flag and no SmartScreen warning. ### What else you need Besides the machine. Nothing else is assumed, and nothing here is installed for you. **An agent CLI** At least one of Claude Code, Codex CLI or Gemini CLI — installed and already signed in. Terminal Deck does not install them, and it never asks you for a login or an API key: the agent signs you in itself, in the terminal. **git** Only for the git panel. Everything else works without it. **The gh CLI** Only for the GitHub panel, which runs your own authenticated `gh`. `brew install gh` on macOS, `winget install GitHub.cli` on Windows, if you want it. **Node 22 or newer** Only to build from source. A downloaded build has no Node requirement. On first launch the app checks what is actually on your machine and, if no agent is usable, shows you what it found rather than an empty window. Settings → Coding AI repeats that check at any time and prints the literal probe it ran, so a missing tool tells you the command as well as the conclusion. ### Installing on macOS - Download and open the disk image Take the macOS build above — `terminaldeck-0.15.0-arm64.dmg` — and open it. Inside is `Terminal Deck.app`. It is an Electron app, so the unpacked bundle is around 300 MB. - Move it to Applications Drag `Terminal Deck.app` into `/Applications`. Running it from `~/Downloads` works, but Applications is where macOS expects it and where you will find it again. - Open it once and let macOS refuse Double-click the app. macOS will say it cannot verify the developer. Click **Done** — not Move to Trash. - Then click Open Anyway Open **System Settings → Privacy & Security**, scroll to the security section, and click **Open Anyway** next to the message naming Terminal Deck. Confirm once more and it launches. After that, double-clicking works normally and you never see the dialog again. That button only appears after step 3, which is why the refused launch is a step rather than a mistake. **Why that step exists** The app is signed with an Apple Developer ID certificate, but it is not notarised — notarising means uploading each build to Apple for a malware scan, and this developer account is not currently able to do that. macOS attaches a quarantine flag to anything downloaded from the internet, and Gatekeeper refuses a quarantined app that is signed but not notarised until you say otherwise. **Right-click → Open no longer works.** It was the standard way past this for years, and macOS 15 removed it — on macOS 15 and later it gives the same dead end as a double-click. Open Anyway is the replacement, and the only route. The signature is real, and you can check it yourself before trusting any of the above: ``` codesign -dv --verbose=2 "/Applications/Terminal Deck.app" ``` It should name `Developer ID Application: Asad Iqbal (6U4VNX5W87)`. If you would rather not take that on trust, build from source below — then the binary is one you produced on your own machine, and no quarantine flag is ever set. ### Installing on Windows Windows 10 or newer, x64. Every step below has been walked through on a real Windows 11 machine. - Run the installer `terminaldeck-0.15.0-x64-setup.exe` asks where to put the app and installs it for your account only, so it does not need an administrator password. `terminaldeck-0.15.0-x64-portable.exe` is the same build as a single file that runs without installing anything — take that one if you would rather not write to Program Files at all. - Click past SmartScreen Windows shows a blue **Windows protected your PC** panel with only a Don’t run button visible. Click **More info**, then **Run anyway**. The portable executable is warned about the same way, every time you move it to a new machine. - Bring your own agent CLI Same as on macOS: Terminal Deck runs Claude Code, Codex CLI or Gemini CLI, and installs none of them. Install and sign in to at least one first, or the first launch has nothing to show you. **Why SmartScreen warns** Signing a Windows build needs a code-signing certificate issued by a commercial authority against a verified identity, renewed yearly. This build has none, so SmartScreen has no publisher to name and no signature to look up a reputation for, and it hides the run button behind **More info** rather than refusing outright. That is Microsoft’s own override, the same shape as the Open Anyway step on macOS: it is how you say you know where the file came from. The one check you can make yourself is the checksum. `SHA256SUMS.txt` on the releases page lists the SHA-256 of every file in the release; `Get-FileHash` in PowerShell prints the one you downloaded. If you would rather not take any of it on trust, build from source below. ### Building from source The repository is the whole product. Node 22 or newer, and about two minutes. #### Run it in development ``` git clone https://github.com/asadev/terminaldeck.git cd terminaldeck npm install npm run dev ``` `npm run dev` starts Electron with a hot-reloading interface. Main-process changes need a restart; interface changes do not. #### Package a real .app ``` npm run pack:mac # unpackaged .app, fastest to smoke-test npm run dist:mac # the disk image: dmg + zip, arm64 ``` `pack:mac` leaves `release/mac-arm64/Terminal Deck.app`; `dist:mac` writes the `.dmg` and `.zip` into `release/`. Both run `npm run build` first. Because you built it locally it carries no quarantine flag, so it opens on a double-click with no Gatekeeper step at all. **Do not run `npm run build` while `npm run dev` is serving.** They share the `out/` directory, and the build overwrites what dev is running out from under it. Stop dev first. #### Checks ``` npm test # vitest npm run typecheck # both tsconfigs ``` ### What this build does not do Worth knowing before you install rather than after. - **There is no Linux desktop window.** The headless host is the Linux route, and it is a real one: it has been installed in a WSL distribution and left running there under a user service, holding agent sessions a phone drives. What it does not have is a window on that machine. - **The host on npm can be behind the desktop.** The one-line install takes whatever version of `terminaldeck` is published to npm, and the desktop app is released on its own schedule. `terminaldeck --version` after installing says which one you have; the host that matches exactly this release is the `.tgz` on the releases page. - **Translations are not built yet.** The app is English only, and the language row in Settings had exactly one option, so it was removed rather than left as a control that changes nothing. - **A Mac has never talked to a Windows PC.** Machine-to-machine pairing has been run end to end against the real trust store and the real pairing desk — with both ends in one macOS process on loopback, which is where every seam that has broken before lives. Across the internet, between two operating systems, it is the same code and it has not been run. - **Neither phone client is in an app store.** The iPhone one is on TestFlight, internal testing only, with no submission made; the Android one is a signed APK on the releases page and not on Google Play. Both are in the list above, with what that means for each. - **Usage, context and session search read Claude Code transcripts.** Codex and Gemini sessions run perfectly well; those panels simply have nothing to read for them. No panel anywhere shows a price — the app reports tokens and context rather than money, on purpose. --- # Changelog Source: https://terminaldeck.dev/changelog ## Changelog Everything below is the real commit history, rewritten so it reads as a story rather than a list of subject lines. The short hashes are the commits themselves. Every release between 0.7.0 and 0.14.0 is on GitHub rather than here — those twelve entries have not been written up yet. 0.1.0 is by far the longest entry because it is the entry in which the application was written; 0.1.1 and 0.1.2 are small on purpose; 0.1.3 and 0.1.4 are where remote access and Windows stopped being claims; 0.1.5 and 0.1.6 are each one thing that had never actually worked for anybody; 0.1.7 is the two that had never worked on Windows; 0.2.0 is mostly subtraction; and 0.3.0 is where the app grew an assistant of its own. Entries before 0.1.9 mention Tailscale, because for a while it was one of two ways a phone could reach a desktop. It is gone. Since 0.2.0 the relay is the network, and nothing about Terminal Deck asks you to install, configure or know about Tailscale or any other VPN. Terminal Deck was built in two long days and an overnight run, in waves of parallel work, so the timestamps say almost nothing useful. Grouping by what changed is more honest than grouping by hour. ### 0.15.0 Sessions on a server stopped dying, and the copilot became a real chat. #### Fixed - A session started on a headless server was closing on its own right after it began. It runs normally now. The copilot’s tools had been rerouting every session through the copilot’s own channel; a plain session gets the simple browser-only tools again, and the copilot runs on its own. - The desktop no longer says a server “can’t run a copilot” or “has no browser”. Both are true now, and the text said otherwise. #### The copilot is a real chat - Your messages sit in a box, its replies are plain text, each one has a copy button and a time, and a suggested command comes with a Run button. There is a typing indicator, and you can attach files. The floating browser pill is gone while you are in it. #### A server is managed from its own page - Restart, Stop and Start the host over the relay — so an offline address no longer breaks it — and Connect GitHub, on the phone and on the desktop. Desktop server management is one place now; the duplicate in Settings is gone. - Sign in and out of your Claude, Codex and Gemini accounts from the phone. It was desktop-only before. - A connected server shows once, under Servers, instead of in both lists. - The usage reading only shows when it is fresh, and hides cleanly when it would be stale. No number left lying, and nothing running in the background to keep it updated. #### Android - The copilot can be restarted; a login the browser asks for can be typed from the phone; tapping a “needs you” notification opens that session; and you can sign out of an account from the bar. #### Removed - **Chat mode is gone** — on the Mac, on Windows and on the phone. A session could be drawn as a conversation instead of as a terminal, read out of the agent’s own transcript file. It never worked smoothly: switching into it often showed nothing, and it went on doing that across versions. The toggle, the pane and the reading over the wire are all removed. A session is a terminal. Talking to the copilot is unchanged — that is its own conversation, on its own screen. ### 0.6.1 The button that lets Windows hold a session inside its folder. #### Windows can hold a session now - On a Mac, a session someone starts from a paired device is held inside the folder you granted and can reach nothing else. On Windows the same mechanism was built and switched off — it needs a one-time administrator permission, and there was no button to ask for it. Settings → Remote → Folders has one: it names the folders it would cover and raises the prompt once. - The screen believes the machine rather than the press. Dismiss the prompt and nothing changes, because the state is read back after asking rather than assumed from a click. - Until somebody presses it, a session from a device on that PC runs unconfined — and the panel says so, instead of claiming the feature does not exist. ### 0.6.0 One pairing decides the copilot, a rented server becomes a machine like any other, and Windows stops being half a product. #### The copilot no longer has a connection of its own - Approving a device as **your own** is now the whole authorisation: the copilot is simply there, on the phone and in the browser, with nothing to connect. A **guest** never sees it — not a disabled tab, not a greyed row. Absent. - Gone with it: the six-digit copilot code, the separate credential, the file that stored them, and every state where a device was paired, trusted, and told to connect first. #### Servers - A machine nobody sits at is now the same shape as one you do: a group in the sidebar, sessions with tab pills, and its private localhost opening in the same browser window. Three things to add one — an address, a username, and a password or a key. Nothing to install on it first. - Signing in with a key no longer asks you to open the file in a text editor. The app reads your key folder and offers what it finds by name, and never offers the public twin sitting beside it. - The headless host installs on a server with no Node on it: it fetches an official runtime, checks its SHA-256 before unpacking, and touches nothing outside its own folder. #### Windows - Attaching a file to a message did not work on Windows at all — browse, drag-and-drop and paste all failed on a path check that assumed one shape of separator. Fixed, in every door. - Every scheduled routine failed on Windows, because the runner kept the agent’s command and threw away its arguments. - Windows now runs the same test suite as macOS on every change. It never did before, which is why the three above went unnoticed. #### Fixed - Token totals were inflated for any project with no session open. - Driving mode ran the scan and never showed the answer — the card that combines it fetched once and never again. - The usage bar would not read on its own for a session started as a shell. ### 0.5.0 A second review of the shipped build, an audit of what the first one actually delivered, and two defects that were live on somebody’s machine while they worked. #### Two things it was doing to a real session - bfdc4c6 It was typing `/usage` into a working session and **leaving the panel open on top of the conversation**, over and over. On an account billed through the API there is no rolling subscription window, so the reading could never be found: every attempt timed out, every attempt left the panel, and it tried again on the next quiet moment for as long as the session was open. The panel is now closed and the close is verified by reading the screen back; “there are no limits to read” is an answer rather than a failure; and an attempt that typed and found nothing stops that session for good. - bfdc4c6 A session where you opened a shell and ran an agent in it — which the app offers a button for — had **no model, effort or usage controls at all**. Two parts of one bar were answering the same question from two places: the account chip read the screen and showed the agent, the controls beside it read the session record, saw “shell”, and drew nothing. #### Every tunnelled response larger than a socket buffer was cut short - bf4e88c Closing the browser’s socket used `destroy()`, which does not flush — anything accepted from `write()` but not yet handed to the kernel was thrown away. Measured at 64 MB into a loopback socket: `destroy()` delivered 327,212 bytes, `end()` delivered all 67,108,864. That number is macOS’s auto-tuned loopback buffer, which is why nothing was ever lost there; Windows defaults to 64 KB and does not auto-tune, so a real page was truncated mid-body. Orderly closes flush now; error paths still discard, so no descriptor outlives its tunnel. - abffc6a A folder with nothing left unread could describe itself as incomplete. When the scan hits its ceiling it drops what is still queued and counts it as unread — and it was counting files it had already read, which can be back in the queue because the operating system replays file events from just before a watch was attached. #### The browser stops caring which machine it is - bfdc4c6 A machine chooser beside the address bar. The start page lists what that machine is serving, and typing `3000` opens its port 3000 — same window, same tabs, same everything. Choosing a machine changes what `localhost` means and nothing else: a public address is the same site from either computer, so it is left alone. - bfdc4c6 Port lists and tunnels are for **your own devices**. A guest gets neither and is never told the capability exists — a port scan can say which program holds a port but never which project, so it cannot be narrowed to the folders a guest was granted. Every port or none, and none is the honest answer. - bfdc4c6 A remote machine now looks and behaves like a project: one machine mark on the heading rather than one on every row, the same menu a local project has, and remote sessions get a tab in the strip. Closing a machine ends its sessions and folds it away without disconnecting it. #### Smaller, and all of it asked for - bfdc4c6 A device waiting for approval says so, instead of appearing only in a settings pane you had to already be looking at. The web app takes an address, with a machine chooser and one theme icon in place of three buttons. Settings → Copilot is half the height it was and every file it lists can be edited in place. The model and effort chips keep their labels at every window width — they had been fading to nothing below 1000 pixels. On the phone the copilot’s tab bar is out of the way of the composer and appears only when it is connected. #### Sixteen tests that could only ever have passed on a Mac - bf4e88c The Windows job gates this installer, and over three days it caught a file URL’s pathname used as a path, POSIX separators asserted against a normalising function, folders named but never created, and tests that waited one tick for an asynchronous parser. It also caught the truncation above, which no assertion was ever going to find. Where the runner itself was simply slower, the timeouts were raised on Windows only, with the measurements written beside them. ### 0.4.0 Ninety-one items from a recorded review of the whole application, and underneath them six doors that were never locked, three tools that could not do what they said, and a list of controls that are now gone rather than pretending. #### Two of three doors were open - 584d01a A paired device could reach **every session on the machine**, whatever folders it had been granted. Creating a session checked the grant correctly. Listing sessions took no device id at all, and attaching admitted any id off that list — so starting a new shell in an ungranted folder was refused while typing into an agent already running in one was not. Underneath all three, a device with no grant record was read as “fall back to whatever this desktop is offering”, which is every open project plus the working directory of every running session. - 584d01a Approving a device wrote nothing. Approval is now the choice itself, and it writes before it admits — the kind of device, then its folders, then the approval, in that order, held by a test that reads the call order rather than the end state, because the end state looks identical when the approval comes first. - 584d01a A device is either **yours** or a **guest**, and it cannot change kind afterwards. A guest reaches only the folders you tick and is never offered the copilot at all — absent rather than defaulted off, because an unchecked box still advertises the thing. Folder containment is re-checked on every keystroke. **Devices paired by an earlier version have no kind recorded and become guests with nothing**: a fix that keeps working for everyone who already had everything has not fixed anything. #### The copilot could not drive, and it was right - 584d01a Opening a page refused whenever no browser tab happened to be open already and told the copilot to ask a person to open one — so it quietly fetched the page instead and said it had not used the browser. Reading a page returned an outline with no page text in it at all, leaving the model guessing CSS selectors to read one line. Handing control back always failed, because the ninety-second window it waited in outlives the sixty-second limit on the call itself. All twenty of its tools were then re-checked by asking a running copilot to use them in words, rather than against a stand-in. - 584d01a It knows where you are: ask what you are looking at and it can answer with the page, the pane, and the address and text of a page it is driving. - 584d01a Driving mode **scans instead of reading to you**. It used to hold each stop long enough for a person to read it. Now it goes through every session at machine speed — including the ones you started yourself — with the window under a field of dots and the quoted line cut clear of them, then returns one answer grouped by session. The reading-time estimate, the pacer and the pace dial are deleted rather than disabled. #### Every sign-in that stuck was the same bug - 584d01a A page’s request to open a window was always refused, and refusing it makes the page’s own call return `null` — so a sign-in library opened its popup, was handed nothing, and waited forever for a message from a window it never had. The destination did open, as a tab, and the sign-in did complete. It had no way to say so. - 584d01a Google sign-in works. It was being served a cut-down flow because the browser announced itself as Electron; nothing else about the browser is disguised. If a site still refuses there is a deliberate handover — finish in your own browser and press a button to bring the session back. - 584d01a Saved logins and browser profiles, with the password store in the main process only: the summary the interface receives has no field a password could be in, and matching is on exact origin rather than domain grouping, so a subdomain somebody else controls cannot ask for a password stored for the parent. #### Counted twice - 584d01a Token totals were over-counted by about 4%. Resuming or forking a conversation copies its history into a new session record, and each record was de-duplicated only against itself. On one real folder that was 11,110 distinct requests counted 11,598 times. Verified against an independent re-implementation written from the file format rather than from this code. #### Built, and reaching nothing - 584d01a Artifacts showed files because nothing in the code held the meaning of the word, and the previous fix had written that conclusion into the source as settled. An artifact is a file an agent produced whole; a file it only edited is a change to your project and sits one chip away with its count. - 584d01a Source control led to Files because the function that fetches a diff had zero callers anywhere in the interface. It is list-left, diff-right in place now. - 584d01a A setting the copilot changes now takes effect on the open window. It used to write the change, report success, and leave the screen as it was until a reload — and it now says which of the two happened, measured rather than claimed. #### Removed, rather than left looking usable - 584d01a The row of controls inside the message box, which repeated the bar above it. Refresh and Reconnect on the phone — one duplicated a gesture, the other was an admission the automatic reconnect might not work. The language picker, which had one option. The bottom band of the browser. The copy of the hooks page inside Settings. And a list of past sessions where every row opened the same one, which was not a bug in the list but the only thing it could do. #### Two platforms, and the runner that proves it - baf9b1d Six tests measured the machine they ran on rather than the rule they were about, and every one passed on macOS and failed on the Windows runner that gates this release: a file URL’s pathname used as a path, a POSIX separator written into an expectation for a function whose job is to normalise, folders named but never created, and a test that waited one tick for an asynchronous parser and hoped. - 3024a58 That last one earned a real method: the plan-limit tracker can now be asked to flush, resolving in the terminal parser’s own write callback, so the wait is exact instead of a guess about how long parsing takes. ### 0.3.0 The app gets an assistant of its own, the browser gets an agent that can drive it, and a long list of screens stop contradicting themselves. #### A copilot - e6996c3 An assistant for the deck itself, pinned above the session list: it can see your sessions, read their transcripts, start new ones and change settings, and it remembers your conversation with it. A developer’s assistant rather than a general one — no inbox, no calendar. It is built as **a real session** rather than a hidden service, which is the whole point: its folder, its instructions, its memory and its transcript are files you can open, and Settings → Copilot shows what it is handed before it starts. - e6996c3 It runs inside the same sandbox as any session started from another device — including from us, and proved against `sandbox-exec` rather than asserted: it cannot read your home directory, your SSH keys, your keychain or another account’s login. Its routines and its action log live outside the one folder it can write to, because a routine it could author unasked is not gated and an audit log the audited party can rewrite is not an audit log. A consequence worth stating: it starts **signed out**, because the keychain is closed to it, and its first screen says so. - f5eee9f The copilot got hands, and three separate features turned out to share one shape — the mechanism present, the connection absent. Its tool configuration was being written and never passed to it, so every sentence about being bounded by a consent gate described a gate that was not in the path. The session fan-out honoured a hidden predicate everywhere and was built without one, so a paired phone could attach to the copilot’s own terminal and type into it. And the hook endpoint baked a port into the hook files that changed on every launch. - f5eee9f **Routines** — saved instructions that run on their own, triggered by something happening rather than by a clock: a session finishing, a session failing, git state changing, a file changing. A schedule is one trigger among several rather than the foundation. One that fires while nobody is at the machine and then needs consent is refused immediately and legibly, instead of hanging on a dialog no one can answer. - a293ec9 It can show you what it found rather than only telling you: it writes a tour and the app plays it, a box around the line that matters, everything else dimmed, pausing the instant you touch anything. What it refused is the part worth reporting. On the first real run the model quoted a line that was never in the terminal and called an idle session blocked; both were dropped before they reached the screen — one by checking the quote appeared verbatim in the source, one by re-checking the claim against the app’s own view — and the recap names the drops rather than hiding them. #### A browser an agent can drive - a293ec9 Driving goes through the browser’s own debugging protocol with a real actionability loop — resolve the element, check it is visible, wait for its box to stop moving, check it is enabled, hit-test where the click will land, then dispatch. Screenshots show you what happened; they are not how anything is decided. - a293ec9 **And no debugging port.** The off-the-shelf way to do this wants a listening socket, and a loopback socket is not a user boundary: on a shared machine another logged-in user connects to it freely, and the renderer is a target on that port holding the preload bridge — so a single evaluate would walk past every consent gate in the process. The engine is in-house for that reason. Measured on the way: a protocol-level navigation to `file:///etc/passwd` succeeds on a view whose navigation guard calls `preventDefault`, because that guard only covers navigation the page starts. - a293ec9 One page, and the two of you take turns on it. Any real interaction of yours takes it back at once and the agent’s next command is refused; while you hold it, every command is refused, reads included. That is deliberate and it is why the rule is not limited to clicks: a screenshot taken while you are typing a password is the leak, and nothing can redact what was never produced. - e6996c3 Draw on a page and send it to an agent, alongside inspect and record. A pane can hold a browser page too — before this a pane could only name a session, so anything else was by definition a closed one and the split collapsed. #### The window - e6996c3 Tabs take Chrome’s shape and join the pane below them. The `+` leaves the strip; a terminal and a globe sit after the last tab; there is one path to a new session and it is the dialog. A tab’s `✕` closes the view and leaves the session running — only the sidebar’s `✕` ends one. - e6996c3 Chrome belongs to the pane rather than the window. An account chip drawn once above two sessions from two projects is wrong for at least one of them with nothing saying which; each pane names its own account, folder and session now, and the main session keeps its chrome in the top bar, which is what makes it read as the main one. - f5eee9f Model, effort, connectors and usage sit in the chrome beside the account, for a terminal as much as for a chat. Clicking a model changes the session that is running — and refuses, saying why, when you have something typed, because the way a CLI is told to change model is a line typed at it and the next Return would have been yours. - f5eee9f **Add your own agent.** The list is no longer four names compiled into the app: name a command and it runs, with the caveat drawn on the row that an agent nobody here has characterised gets a terminal and not a model picker. The command is resolved against your login shell’s `PATH` before it is saved, so a name that cannot be found is refused at the form rather than becoming a row that dies when you pick it. - e6996c3 Attach a file from anywhere — the real system dialog, drag and drop, or paste. The picker had only ever offered files inside the project. Notifications open as a popup instead of taking over the window, and Settings is one surface rather than three greys pretending to be one screen. #### Fixed - e6996c3 Hooks stopped working every time the app restarted. The endpoint took a new port on each launch and that port was written into the hook files, so session-finished events silently never fired — and a credential was written world-readable into an agent’s settings file. The endpoint is a socket now, at a path the kernel cannot hand to a stranger. - f5eee9f Two faults that only appear when you run the app rather than read it. Transcripts under a symlinked path were invisible to usage, chat, alerts and loop detection, all of which reported the miss as nothing to see — `/tmp` is symlinked on every Mac. And a shell session was handed another session’s conversation, complete with 780,000 tokens belonging to a session that had already ended; shells write no transcript, ever. - f5eee9f A web page could paint over the terminal, and the trigger was `⌘R`: React cleanups do not run on a page unload, and the host is the same object across a reload, so both safety nets missed for the same reason. Visibility is decided by the main process now, from the renderer’s document generation, which a renderer that stops talking cannot defeat. A 21-pixel tooltip also used to blank a 587×644 page, because any intersection counted as total coverage. - 34ff13f A confined session died on its first turn on macOS, before producing a token, because the agent CLI keeps scratch files at a path outside every sandbox. The same fault broke sessions started from a paired phone. A claim about the sandbox also moved off the terminal that could not hold it. - 92c6b6c Wait for the shell before typing at it, rather than sending the first keystroke into a terminal that is not listening yet. - a293ec9 Windows could not run hooks at all: a socket path there maps to a named pipe, which rejects a filename, and the tool the hooks used cannot reach a pipe. Only the program had to change, which an experiment established rather than a document. Two boundary tests were also passing without checking anything, their setup returning early off macOS and leaving the path-agreement case asserting against an empty string. - e6996c3 Screens that contradicted themselves: the model picker typed into a live prompt and left a confirmation dialog standing for the next Return to answer; Power’s notice denied its own switch; a language dropdown had one option; a debug file offered Reveal when it did not exist; the file viewer drew characters the file did not contain, because programming ligatures were on in a pane whose whole job is showing text verbatim. Clicking a file in the tree also closed the Files page, because navigation was a toggle. #### Removed - e6996c3 Every price and cost figure, finished. One of the two could not be computed at all — nothing is published from which to derive what a subscription costs per token — and the other told somebody on a flat monthly fee that they had spent money they had not. Token counts, cache share and the context window stay, because those are facts rather than inferences. #### The clients - 7df2fe5 iOS gets a Localhost tab and a three-tab shell — and the first build any of that had ever been compiled in. - b8e542e The web client catches up: localhost ports grouped and nameable, and more than one machine at a time. - 9ede37c One live pairing code per machine, and a code that admits when it cannot find the machine it names rather than failing blankly. - a293ec9 The copilot reaches your phone over its own connection — a separate code, a separate credential and four independent refusals, rather than a checkbox on the session pairing. It carries no device id, so a leaked one is half a pair rather than a bearer token. - 4a50e69 9,136 tests passed and the release still failed, on a directory removal. The release script is the thing that has to survive a green test run. ### 0.2.0 The release that took things away. A pairing code with one method instead of three, a network with one path instead of two, and every price in the app deleted. #### Pairing is six digits, and nothing else - 3babd08 One way in, and it is six digits on a numeric keypad. The **QR code and the pairing link are gone entirely** — deleted, not hidden: the QR encoder, the iOS scanner and its camera permission, the Android scanner and its `CAMERA` permission, and every `terminaldeck://pair` route into a credential write. The QR did not work, and a link was a two-hundred-character string carrying a live bearer secret that had to travel through a messaging app to be useful — which is a pairing token somebody else’s server then has a copy of. - 3babd08 Six digits is a million codes where eight characters were 1.1 trillion, a reduction by a factor of 1,099,511, and the number is stated rather than buried. What makes it sound is unchanged and now pinned by tests rather than assumed: a code lives sixty seconds, is single-use, and dies after five wrong guesses — the code dies, not the guesser, so minting a fresh key for every attempt buys no fresh budget. Redeeming one still only produces a pending device that a human approves on the machine itself. - 9ede37c One live code per machine, and a code that admits when it cannot find the machine it names rather than failing blankly. #### The relay is the network - 3babd08 The “Direct on your tailnet” card is gone from the app. There is one path to a machine now: Terminal Deck’s own rendezvous relay, which both ends dial out to. No port forwarding, no VPN, no account, and nothing anybody has to install or understand before a phone can see a terminal. Everything it carries is sealed under keys the relay never sees, so it can end a session and cannot read one. - 3babd08 Machines and Remote are one section, in the sidebar rather than inside Settings. Pairing a device is something you do, standing at two keyboards — not something you configure once and forget, which is what everything else in Settings is. #### No prices anywhere - aa08a84 Every dollar figure is deleted — the rate card, the per-session and per-day spend, the money column in the session inspector and the cost tile on the dashboard. The API number was correct arithmetic and it still misled: almost everybody running this is on a subscription, and telling someone on a flat monthly fee that they spent four figures states a number that never left their account. The subscription number cannot be computed at all, because no plan publishes a token allowance or a per-token value to measure a session against. - aa08a84 What replaced it is what was always underneath: tokens split by fresh input, output, cache read and cache write; the share of the prompt served from cache; the request count; what the project has moved today; and context-window pressure with the compaction count beside it. The dashboard tile is called Usage. Deleting the rate card also removed a live bug — a time-boxed price due to revert on a date nobody was watching, for an increase that has since been cancelled. #### The window - aa08a84 Overview is a live board of running sessions — what each agent is doing, how long it has been doing it, and which one is waiting on you. Deliberately no progress bar: an agent does not report progress, and a number the app invented would be worse on that screen than no number. - aa08a84 Search is replaced by Artifacts — every file your agents wrote or changed, with the diff of each change. Searching past sessions moved into the command palette under `?`, one keystroke from what you were already doing. Search’s own results had never been clickable. - 8e49830 Sessions can be dragged into the top strip. The strip was built and nothing in the app was draggable, so it looked finished and did nothing. Promote and demote also work from a row toggle and `⌥←` / `⌥→`. - aa08a84 Windows has one title bar instead of an OS strip, a menu strip and the app’s own chrome, with real minimise, maximise, close and snap layouts. About nine hundred words of on-screen prose were removed, with every warning kept. - c620f03 An account belongs to an agent. Adding one asks which — Claude Code or Codex CLI, both verified to isolate a login. Gemini is listed and refused, with the reason on the row: its token lives in one keychain slot no config directory moves, so a second login would overwrite the first rather than sit beside it. - d060cff Light and dark in the web client, following the system by default. #### Fixed - aa08a84 Keep-awake held nothing. The app’s own power-save blocker was gated behind a privileged system setting, so on every machine where that had not been granted — including every machine on first run — no lock was held at all. - aa08a84 Sessions did not come back on Windows: a WSL path was resolved against the Windows host, so `/home/you/x` became `C:\home\you\x` and matched nothing the Linux agent had written. The same fault emptied the Files page there. - c620f03 Adding an account always signed you into Claude. The preload dropped the options object, so the provider never crossed the bridge — signing in beside a Codex account opened a Claude session, which then correctly refused the account and silently discarded it. - 64da716 Wait for the shell before typing at it, rather than sending the first keystroke into a terminal that is not listening yet. #### Removed - 942aca5 The GitHub notifications bell. GitHub’s notifications endpoints accept only classic personal access tokens, a GitHub App user token is not one, and no permission can be added to change that. Sign-in is the GitHub App and nothing else now. ### 0.1.9 Four batches of work, and the release where one machine could first drive another. #### Machines, devices and what they may reach - 43273bb One computer can be paired to another and drive its sessions from the Machines panel. A typed code cannot carry an address, so it names a slot at the relay: the machine showing the code sits in that slot for the sixty seconds the code lives and answers with its real URL, host id and public key. A hostile relay cannot answer in its place, because the responder’s key pair is derived from the code as well. - 9279d76 Per-device folder grants: the machine that owns the files decides which folders each paired device may **start a session in**. Read that literally — it is organisation, not a sandbox, and nothing in the app says otherwise. A shell that starts in a granted folder can `cd` anywhere the user account can reach. The security boundary is the one that was already there: pairing, plus a human approving the device on the machine itself. - 9279d76 The credential proxy — their GitHub, from their device, never yours. A session started from another device is handed its own git configuration, so `git push` cannot reach the owner’s account through the credential helper, the rest of the global config, `gh`, or ssh. When git needs a login, the request crosses the sealed channel to the device that started the session and is used once in memory; nothing is written to the host’s disk, so revocation is disconnection. - 43273bb Sessions in WSL, routed by the folder rather than by a switch. A Linux path launches through `wsl.exe` inside the distribution; a Windows path launches through `cmd.exe` as before. There is no “use WSL” toggle on purpose — a toggle is what lets the two disagree. #### Two things that were believed and were wrong - 0761f87 Notifications were never a signing problem. The theory was that macOS keyed authorisation on a signed bundle identity and refused an ad-hoc one. Measured instead: macOS registers the ad-hoc bundle perfectly well and had simply put up its authorisation prompt, which arrives as a notification banner with Allow hidden behind an Options disclosure nobody had opened. Clicking Allow was the entire fix, and macOS’s own notification store recorded the deliveries afterwards. - 72b1015 The app stopped saying “Sent.” for a banner it never watched arrive. #### Windows, and the guards that were not running - e50f276 Answer for the platform you were asked about, not the one you are running on — with two follow-ups for the same fault in other places. - 57ab908 A guard that cannot run on Windows is worse than no guard, because it reads green. Several were rewritten so they run on the platform they were written to protect. - 5db67d0 Three ways this Mac lied about being a clean machine, found and closed, so a passing suite here means something on somebody else’s. #### Tailscale leaves the product - 388ee5b The browser client dials the relay, and Tailscale leaves the product’s face. Nothing anyone downloads needs it, needs to know what it is, or is asked to install it — a point 0.2.0 finished by deleting the tailnet card outright. ### 0.1.8 One fix, and a test so it is the last time. #### Wording that crosses to a phone - fa2d5c8 Nothing sent to a phone calls a Windows PC a Mac any more. Fourteen sentences in the remote server named a platform and ten of them were sealed up and read on the phone — “Pair it again from the Mac.”, “This Mac cannot start sessions from a phone.” — and one phone now holds several machines at once, so those could appear directly beneath a row naming a Windows PC. The phone already prints each machine’s own label beside anything it says, so wire copy names no platform at all; copy the person at the keyboard reads keeps the noun and asks the code that knows which one. This was the third sweep of the same rule by the third pair of hands, so it is scanned now rather than swept again: a string in a module whose output crosses the wire may not name a platform, and the test says which line when one does. Run against the previous release it reports all fourteen, including four a careful reading of the diff had missed. ### 0.1.7 The release in which Windows stopped being the platform where two things quietly did not work. Both were measured on a real Windows machine rather than reasoned about. #### The localhost tunnel - 81a4ced It reaches a dev server on `::1`, which on Windows is most of them. Windows resolves `localhost` to `::1` before `127.0.0.1`, so `vite`, `next dev` and `node --host localhost` bind IPv6 and nothing else — and the tunnel only ever dialled `127.0.0.1`. The port was scanned, listed, offered to the phone, and then refused the moment it was tapped, with nothing anywhere saying why. A port that is listed and unreachable is worse than one that is not listed. The scan now carries the address family it always knew and the tunnel dials the loopback the port is actually on, deciding once per tap by connecting rather than per browser connection by guessing; when neither loopback answers the tunnel is refused with a sentence naming what was tried. Nothing changed on the wire, and nothing changed about the tunnel being a byte pipe rather than an HTTP proxy — which is what keeps hot reload, server-sent events, cookies and the WebSocket upgrade working. Proved on a real Windows machine in all three ways a dev server is started, pulling a real HTTP response and a real `101 Switching Protocols` upgrade through the tunnel each time. #### Updating in place - 81a4ced Windows installs its own updates without anyone clicking an installer. Finding and downloading already worked there; installing did not. The app quit, an installer window nobody asked for opened, and it sat on a page waiting for a click — fifty seconds later the setup was still running, the old version was still installed, and there was no app on screen at all. It installs silently now and the app comes back, which is what a button saying Restart promises. Watched working end to end: an installed build took itself from 0.1.7 to 0.1.8 through its own panel and relaunched. - 81a4ced The portable Windows build no longer offers an update it cannot install. It is the same build as the installed one and carries the same release feed, so it reported itself updatable — and an update on Windows is an installer, which would have put a second, installed copy of the app somewhere the user did not choose while the portable executable they were running stayed old. It says plainly that a portable app cannot update itself and points at the releases page. ### 0.1.6 One fix, for something no release had ever contained. #### The phone client - c7054e2 The page you open on your phone is in the app now. It never has been: 0.1.3, 0.1.4 and 0.1.5 all shipped without it, so the tailnet address the Remote panel prints for you to open answered with nothing at all. The native phone apps were unaffected — they speak the sealed protocol directly and never fetch that page. Nothing failed loudly enough to notice, because the client is build output and therefore gitignored, so the line in the packaging allowlist that names it matched zero files in a clean release checkout — which looks exactly like a line whose files were all excluded on purpose. It only ever worked on a machine where somebody had built the phone client by hand at some point. Packaging builds it now, and the release check fails if the built page is not in the bundle. ### 0.1.5 The release that made remote access work without anyone switching it on, which is the whole point of it. #### Remote access - 3938381 The desktop dials the relay at launch now. The relay had existed for a day and this Mac had never dialled it once outside a test: `start()` ran only from a two-press switch in Settings → Remote, and nothing re-ran it on the next launch, so a computer that had been restarted simply was not reachable. Measured on the machine this was found on — the host identity on disk, two paired iPhones in the trust store, the relay up, and not one socket to it. A phone attaching to that host was attaching to something that was not there, which is exactly what “connected, but showing me old output” looks like from the sofa. - 3938381 Dialling on its own exposes nothing, which is why it can be the default: the relay learns that a host is online and no more, and a device still has to be paired and approved before one byte moves. Switching it off is remembered, so off still means off. Proven on the packaged app rather than asserted — an established socket from its own process to the relay, the panel reading Connected against this machine’s host id, and the relay’s host count rising when the app launches and falling when it quits. - 3938381 The Remote switch stopped describing the opposite of its own position. It read “Off by default. Nothing can reach this Mac while it is off.” beside a switch that is now on. - c0d88f5 On the phone, the keyboard is raised before you type at a terminal, and the rename alert is presented from the root rather than from a view that may already be gone. ### 0.1.4 The release that works on Windows. Everything 0.1.2 could not do there — find an agent CLI, start a session, reach Settings, quit from the menu bar — works now, on a real Windows 11 machine rather than in a runner. The whole test suite runs on a Windows runner and gates the installer, so a red Windows test stops the release rather than being waved through. One Windows fault is still open and is named at the bottom of this page. #### Windows - 53016ed Agent CLIs report their version. They are `.cmd` shims on Windows, which Node refuses to spawn without a shell, so every version column was blank. The shim path is quoted too, or the fix would still have failed for anyone whose Node lives under `C:\Program Files`. - 53016ed No more console windows flashing over whatever you were looking at. Seven child processes were spawned without `windowsHide`, including the Tailscale status check that runs the whole time the remote panel is open. - 53016ed Settings, Keyboard Shortcuts, About and Exit are back. All four lived only in the macOS application menu, which Windows drops wholesale, so the menu bar had no way to reach Settings and no way to quit — and `Ctrl+,` and `Ctrl+/` were unregistered along with them. - 53016ed The diagnostics bundle prints a readable `PATH`. It was split on the POSIX separator, so every entry was torn apart at its drive letter — in the one file somebody attaches to an issue that says a CLI cannot be found. - 9555b0b Turning remote access on is instant again. It used to sit for fifteen seconds with the panel spinning and then say “Tailscale did not answer”. Tailscale had answered, immediately: Serve is not enabled on your tailnet, with a link to switch it on, and then waited forever for someone to click it. The output is read as it arrives now, so the refusal comes back in under a second carrying Tailscale’s own words and the link. Remote access was never blocked by this — the relay does not go through Tailscale — but nothing said so for fifteen seconds. - 9555b0b Starting a session from a phone no longer refuses a folder that is on the list the phone is showing. Two spellings of one Windows folder — a lower-cased drive letter is enough — compared as different directories, and the refusal said “open it on the Mac first” about a folder already open. - a0a114a The last six places that build a child process environment go through one helper, so Windows is never left holding two spellings of `PATH` with no rule about which one the child searches. It affected the GitHub panel, the git status poller, the readiness checks, Copilot detection and every stdio MCP server. - a206d5e, 2c96717, 677aa94 The Windows test suite became a gate. The suite was ported and the release workflow no longer lets that job fail without stopping the build. The cases that cannot mean anything on Windows — POSIX file modes, the POSIX shell the hook command is written for, the macOS-only updater — are skipped individually, each saying why, rather than the suite being waved through as a whole. #### One phone, several machines - 4e45d5f A phone paired to a second computer used to quietly drop the first. Pairing adds a machine now: each owns its own transport, its own sealed channel against that machine’s static key and its own sessions, so two machines cannot read each other’s work. All of them stay connected, so switching needs no handshake, and the keepalive they need is folded into one app-wide tick — 144 radio wake-ups an hour at one machine and at five, measured rather than asserted. - 4e45d5f Inspect mode, on the phone. Tap an element on the tunnelled page, say what should change, and it lands in a terminal on the desktop as exactly one line. The rule is transcribed from the desktop’s own capture panel rather than approximated, so both clients hand the agent identical strings — a newline would submit the prompt early and an escape would repaint the terminal it arrived in. - 4e45d5f The phone’s Connected badge no longer claims a connection the app does not have. Observed against the live relay: no guest attached for a sustained forty seconds while the app read Connected. “Online” had been decided before the phone went in a pocket, and nothing tells a socket that a carrier NAT reclaimed it. Resuming now doubts the channel, says “Checking”, and probes immediately. - 9e00c74 The Android client gained the same several machines the iPhone had. #### Surfaces that said something they did not mean - 0f0cdcd Every page has one blank, and it is the same blank. Empty states were four different designs across ten panels — GitHub drew its own glyph, title and button, MCP printed a bare sentence with literal backticks in it, the board dropped inline text into a column. There are two now: one for a page with nothing on it, one for a section of a working page or a page still reading, parked at the same height so the answer lands exactly where “Reading…” was. - 0f0cdcd The inspector stopped jumping under the pointer. It re-centred on every tab press, because the tallest of its four panels ran past the floor and moved the sheet 50px each time. A definite height parks it and the body scrolls. - 0f0cdcd Context → “How it filled” draws something. It plotted a percentage of the context window against a fixed 0–100 axis, and a healthy session peaks near four percent, so every point landed within a pixel of the baseline. The axis scales to the peak now, and both the ticks and the caption print the ceiling — the honesty problem was never the scale, it was a chart that drew nothing. - 0f0cdcd “Unknown” is gone where the answer was never knowable. Fast mode said Unknown beside three siblings that always resolve, which read as this app failing; the CLI announces fast mode only when it changes, so a session that has never been told has nothing to report. It says “Not reported” and the menu says why. The no-project screens stopped offering four ways to open a project, and Settings → Help stopped drawing its own copy of the window’s own menu. #### The build, and quitting - 2004da5 The installer is checked for what is inside it, not just that it exists. A platform `files:` list in electron-builder replaces the root allowlist rather than extending it, which shipped the whole repository — a 1.0 GB app — until it was caught. Both platform blocks were fixed, and CI now fails if a build carries `ios/`, `android/`, `relay/`, `src/` or `build/`, if the app cannot start, or if an artifact lands outside a sane size band. - 2004da5 A release build can be produced from a commit without publishing it. The workflow can be dispatched by hand and publishing stays gated on a version tag, so “let me check the installer size” and “ship it to everyone” stopped being the same button. - ae86843 Quitting with a live session no longer prints a wall of errors. Stopping the app while a terminal was still producing output threw `Render frame was disposed before WebFrameMain could be accessed`, once per message still in flight, because the main process was broadcasting into a window that had already gone. Nothing was lost and nothing was corrupted — the errors were the whole symptom — but they were the last thing a packaged build printed. - ae86843 The debug trace no longer writes unless you ask for it. A build could leave an `ipc-trace.log` of 12 MB and growing in its application-support folder with Debug mode switched off. It is written only while Debug mode is on now, capped at 4 MB with one previous generation kept, and listed in Settings → Advanced so it can be found and cleared. A file left by an earlier version is deleted the next time the app starts with tracing off. ### 0.1.3 The release where remote access started working, and the release where Windows was launched for the first time in this project’s history — and then fixed. #### Remote access - 9728d2b Remote access had never worked, on any platform. Electron links BoringSSL, which ships 28 ciphers and not one ChaCha, so `createCipheriv('chacha20-poly1305', …)` threw `Unknown cipher`, a silent catch swallowed it, and every relayed handshake closed with nothing on the wire and nothing in the log. The whole test suite passed throughout, because vitest runs under plain Node. The cipher now comes from one implementation used in every runtime, with no “native when available” fast path, and the suite runs the sealed channel under Electron’s own Node and fails the build if that stops being true. - 9728d2b A paired device no longer has to be paired again after a restart. Host identity validated its stored keypair by running a handshake, so the bug above quarantined and regenerated a perfectly good identity on every launch, orphaning every device paired to it. - 9728d2b Both phone clients were one byte off the wire spec, and both stand-in hosts shared the bug — so the fixtures agreed with each other and disagreed with reality. The stand-ins import the real framing now rather than reimplementing it. - 9728d2b See the desktop’s localhost on your phone. Tap a port and the phone dials `127.0.0.1` on the desktop through the sealed channel. It is a raw TCP byte pipe rather than an HTTP proxy, so WebSockets, hot reload, service workers and cookies all survive untouched. Only `127.0.0.1` is dialable, only ports something is listening on right now, and only after a person taps one. - 9728d2b Start a session from the phone. Both phone clients had had a New Session button for a while, gated on a capability no desktop advertised — so it could never appear, and each client had invented its own frame shape against its own stand-in. There is one shape now, parsed and narrowed like every other frame; the desktop advertises it only when its session layer can actually start one; and a session started this way is a real pseudo-terminal made by the same call the desktop’s own button makes. A phone may name a folder only if the desktop is already offering it. - 1880202 A session started from a phone appears on the desktop, without focus, so answering something on your phone never pulls the desktop out of the terminal you were typing into. #### Windows, run for the first time - 9118e11, e18b47e The two faults that made 0.1.2 unusable on Windows, found by installing it there and fixed here. `which` was spawned as a literal command; `spec.bin` was spawned where `spec.spawn` was meant, and an npm-installed CLI answers a Windows `PATH` lookup with a `.cmd` shim that `CreateProcess` will not run; and three sites that spread `process.env` over a `PATH` key lost against Windows’ own `Path`. 31 Windows test failures became 0. #### The window - 9728d2b The desktop speaks Apple’s language: one sidebar, one toolbar, Settings bottom-left, liquid glass, and both themes first-class. Three entries came out of the reachability allowlist and none went in — each was a setting that turned nothing on. - 9728d2b Roughly 87,800 fewer wake-ups a day. One shared renderer scheduler where N jobs cost one wake-up rather than N, nothing armed at all while the window is hidden, and a panel that polled a channel already pushing to it now subscribes instead. The remote panel alone was a 500 ms interval — 172,800 wake-ups a day to move labels that mostly change once a minute. - 1880202 Every page got a designed empty state instead of a bare sentence floating in the middle of the window; pages and Settings hold a measure and centre it, so a wide display no longer parks the content in the top-left corner; and the update notice became an inset card wearing the same glass as the sidebar rather than a full-bleed grey strip. #### Packaging - 9728d2b, 1880202 Every macOS build had been shipping the whole repository. electron-builder’s per-platform `files:` list replaces the root allowlist rather than extending it, so the `mac:` block had no allowlist at all — invisible until the phone clients existed, at which point the app reached 1.0 GB. It is 287 MB now, and the identical latent bug in the `win:` block was fixed in the same pass. - 8a988ba Stopped tracking a `node_modules` symlink that pointed into somebody’s home directory. ### 0.1.2 The first release with a Windows build in it. The installer and the portable executable are both x64 and both unsigned. Running them on a real Windows 11 machine is what found the two faults that made this version unusable there — every agent CLI reported missing, and New Session failing — and both were fixed in 0.1.3. #### Windows in the release - 1ecca75 Version 0.1.2 — the first tag whose artifacts include an x64 installer and a portable executable alongside the macOS disk image. - 03b6a42 Let the Windows installer build while its test port is unfinished, so a failing test on a platform nobody has shipped to does not hold back the artifact people are asking for. - ef0312d, 5648d65 Stopped the test suite assuming POSIX where the product does not, and fixed two tests that depended on the machine they happened to run on. - af1f02d Put back the workflow step that creates the release, missing since the previous tag. ### 0.1.1 The release that taught an unsigned build to update itself. #### Updates - 40e84d9 Update inside the app, without Squirrel and without a signature. Squirrel.Mac refuses to replace a bundle it cannot verify, which is every unsigned build — but the feed, the archive and its SHA-512 are all public, so the app reads the feed itself, proves the bytes, and swaps the bundle. The old app is moved aside rather than deleted, and moved back if the new one fails to land. - b161be7 Build Windows on Windows. Cross-building was producing an artifact the prebuilds rule then broke; building it natively in CI does not. ### 0.1.0 The first version that is a usable application. It runs from source and packages into an unsigned macOS build, published as a disk image; it predates both the Windows target and the in-app update path, so it can neither be installed on Windows nor replace itself. #### The window, and the processes behind it - 473d9bb The Electron shell: multiple terminal sessions in one window, each a real process, with the tab strip and scrollback that survive switching away. - 51f59c5 Projects and window bounds persist between launches, installed agent CLIs are detected through the login shell’s `PATH`, and a strict content security policy is set from the main process — permissive only while developing. #### Knowing what a session is doing - b72448a Session status arrives: working, waiting, needs input, exited — with tests, and a full build plan for the eight phases that follow. - 17a418d The first real correction. Status had been classified from the tail of the output stream, which is meaningless for a full-screen program that repaints by moving the cursor. Each session now feeds a headless terminal emulator and the status is read from the rendered screen — including the discovery that Claude Code draws its prompt as `❯`, and never on the last line. #### The features, built in waves - 769ab1d Six modules built in parallel and wired in: transcript reading, cost and context maths, the git panel, the file tree and viewer, project search, and the command palette. - 678e6bd Session resume — continue the last conversation in a folder rather than starting a fresh one. - 6265ba5 Wave two: the session inspector, the GitHub panel driven by `gh`, AI-readiness scoring, the project dashboard and the swarm view. - b601f77 Wave three: full-text search across past transcripts, the alerts panel, agent profiles with isolated logins, provider hooks, the MCP inspector, the first embedded browser, and one keymap table that replaced three drifting copies of the shortcut list. - 7502ef7 Wave four: the settings window, a browser that is a real browser rather than a panel, the new-session dialog, in-app help and a debug panel. #### Making it look like one application - 0c5724c The Notion-style palette — near-neutral surfaces, warm grey text, hairline dividers, one blue reserved for selection and focus — and a side panel that collapses and resizes. - d21799c The app shell proper, which made every feature reachable. Several panels existed and worked before this and simply had no way in. - b51b44f The view switcher was styled as a control, because it read as a caption. - a2971c3 Tabs moved into the window header, and sessions and browsers were merged into one strip. Two separate controls for “what do I have open” meant the browser was effectively unreachable. - 0bf3904 One header bar. Overview and Board moved to the side rail, where views of a project belong — the tab strip is for windows you opened. #### Making every panel actually work - f8ac57e Fixed the panels and views that rendered nothing. Typechecks and tests had all passed; the failures were contract mismatches at the bridge and runtime throws, neither of which a compiler sees. - 56e5273 Every remaining mismatch between the interface and the main process fixed, and the app made installable. - 6f4e500 Tracing across the bridge, so a panel that silently fails says why instead of showing an empty box. - 2abed94 The browser fix worth naming: a request-style call answered by an event-style handler rejects every time, so every browser action failed. - b986623 A test over the seam itself, so a method the interface calls and the main process does not implement fails a test rather than a user. - 64b2370 The browser hides while a dialog is open. It is a native view layered above the interface, so pressing ⌘, on a browser tab dimmed the window and showed nothing — settings were underneath the web page. #### The browser grows up - b98be29 A Setup section that reports what your machine actually has, cookie import from an installed Chrome so a dev server behind a login does not need signing into twice, and per-tab isolation for when you need two accounts at once. #### Chat mode - 6be5d78 A session as a conversation, read from Claude Code’s own transcript — prompts and replies, none of the tool plumbing. The address bar in the browser stopped guessing at port 3000 and started listing the dev servers actually listening. - c4c15f6 Chat mode gained a composer. It writes into the session’s own terminal, so the two views never disagree about what was said. #### Housekeeping - 3472525 A README and a working guide for the repository — which also closed two of the app’s own readiness findings against itself. - 6b99916, d951c4c, a3e6acb The roadmap kept current after each wave landed. - 41a96fc Renamed to Terminal Deck. The name lives in exactly one file, so this was an edit rather than a hunt. ### Not in any version yet Listed here so the absence is on the record rather than discovered after installing. - Peer-to-peer session sharing with another person. Reaching your own machine from your own phone is a different thing, and it works from 0.1.3 onwards. - Updating in place on Windows. On macOS it works, from 0.1.1 onwards. On Windows the check is wired to electron-updater's installer and nobody has watched it run, and the portable executable is a loose file nothing can replace for you. - Translations. - A signed installer on either platform. The disk image, the Windows installer and the portable executable are all built; nothing signs any of them, so macOS needs the right-click step and Windows needs the SmartScreen override. - Split panes. The layout code and its chords are in the repository, but nothing in the app renders the split view, so there is no way to reach it. - Anything at all on Linux. Intel Macs are deliberately out of scope. --- # Roadmap Source: https://terminaldeck.dev/roadmap ## Roadmap Nine phases. Eight are essentially finished, and the ninth is mostly what stands between a signed-but-unnotarised disk image and one macOS will open without an argument. Items have moved back out of Done before now, after an audit found them unreachable from the running app; one has moved out of Done in the other direction, deleted rather than shipped. ### Done #### 1 · Foundation - Electron, React and TypeScript, built with electron-vite - Process ownership, with scrollback replayed across tab switches - Provider layer — Claude, Codex and Gemini detection, login-shell `PATH` - Sidebar, tab bar, status dots, empty state - Design tokens in dark and light, self-hosted open-licensed fonts - Atomic JSON store; projects and window bounds persist - Content-security policy set from the main process #### 2 · Session intelligence - Status detection in the main process, so unrendered tabs stay accurate - Provider picker — per session, per project, global default - Session resume - Preferences, and live re-theming that recolours terminals without a restart - **Desktop notifications and sounds** — a banner has now been watched appearing, from the installed build, and macOS's own notification store recorded the delivery rather than the screenshot being the only evidence. The earlier failure was never a signing problem, which is what it was believed to be: macOS had put up its authorisation prompt as a banner, with Allow hidden behind an Options disclosure nobody had opened, so every notification was being dropped silently. Clicking Allow was the whole fix. - **Session titles derived from the task, and unread indicators** — both driven in the running app on 14 Aug 2026 rather than read off the source. A shell session renamed its own tab from `Session 1` to the task echoed into its terminal, and the session left running in the background picked up an unread dot the moment it produced output while another tab was in front. `App.tsx` imports `AutoTitler` and `UnreadTracker`; when this line first appeared, `renderer/unread.ts` had no importers at all. #### 3 · Usage, context and telemetry - Transcript watcher - Token accounting per session, project and day, split by fresh input, output, cache read and cache write - **Prices removed, deliberately.** The app showed an API cost and it was real arithmetic, but almost everyone running it is on a subscription and a flat monthly fee does not become a per-session dollar figure — and no plan publishes a token allowance to compute the other number from. One was misleading, the other unknowable, so both are gone and the app says nothing about money. - Token and context-window monitoring with a bloat warning - Session inspector — timeline, token breakdown by model, cache hit rate, tool-usage statistics - Deep search across past transcripts, from the command palette - Smart alerts for missing tools, context bloat and session health #### 4 · Project workspace - Git status watcher and git panel - File tree and viewer with syntax highlighting - Quick open and command palette - GitHub integration through `gh` - AI readiness score with one-click fixes - `.deckignore` with gitignore semantics, read by the file tree — quick open and the watchers still do not consult it #### 5 · Dashboard - Customisable project overview on a drag-and-drop widget grid - Widgets for readiness, sessions, usage, git and GitHub - **Kanban board removed, code and all.** It was built — three columns, search, tag filtering, and cards that could start or resume a session — and then deleted on 15 Aug 2026, along with its state module, its main-process store, its overview widget, its menu item and its shortcut. Not wanted: a task board is a thing you keep up to date by hand, and nothing else in this app asks that of you. It went in one piece because a half-removed feature leaves rows that open nothing, which is worse than the feature was. #### 6 · Many sessions at once - Swarm mode - Accounts with isolated logins, per agent — Claude Code through `CLAUDE_CONFIG_DIR` and Codex CLI through `CODEX_HOME`, both measured against the real CLI. Gemini CLI is listed and refused, because its token sits in one keychain slot no configuration directory moves - Full keymap and a shortcut reference sheet #### 7 · Integrations - Hooks installed into each provider's settings, namespaced so they never collide with another tool's - MCP client and inspector - Embedded browser with element inspect - **An agent that can drive that browser** — through the browser's own debugging protocol with a real actionability loop, not through screenshots. One page, and the two of you take turns on it: any real interaction of yours takes it back at once, and while you hold it every command is refused, reads included, because a screenshot taken while you type a password is the leak - Draw on a page and send it to an agent, alongside inspect and record - Chrome configuration import #### 7b · The copilot - An assistant for the deck itself, built as a real session rather than a hidden service — its folder, instructions, memory and transcript are all files you can open - Sandboxed like any session started from another device, including from us, and proved against `sandbox-exec` rather than asserted. It starts signed out, because the keychain is closed to it, and it says so - Routines — saved instructions triggered by a session finishing or failing, git state changing or a file changing. A schedule is one trigger among several, not the foundation - Agents you add yourself: name a command and it runs, with the caveat on the row that an agent nobody here has characterised gets a terminal and not a model picker #### 8 · Away from the desk - Pairing by six-digit code — sixty seconds, single use, dead after five wrong guesses, and it only ever produces a pending device a human approves on the machine itself - **QR codes and pairing links deleted.** The QR did not work, and a link is a live bearer secret that has to travel through a messaging app to be useful. Both are gone from every client, along with the camera permissions they needed. - Our own rendezvous relay at `relay.terminaldeck.dev` — both ends dial out, nothing is port-forwarded, and no VPN or other network has to be set up - Noise IK sealed channel, written four times in four languages and byte-identical across all of them, so the relay carries ciphertext it cannot read - Web client at `app.terminaldeck.dev` — session list and a real terminal in any browser, installable as a PWA, light and dark - Native iPhone client on TestFlight — VT100 emulator, key bar and grid, gesture scrolling and selection, automatic reconnect - Android client — a signed APK published with every release and offered on the download page - **The headless host, installed rather than built.** One line puts it on a machine with no screen: it fetches its own Node when there is none, writes nothing outside your home directory and never runs as root. It has been left running in a WSL distribution under a user service, holding real agent sessions a phone drives. - One phone can hold several machines at once, each with its own sealed channel - Localhost tunnel — reach a dev server on the desktop from the phone, over a raw TCP pipe, on macOS and Windows both - Per-device folder grants, deciding where each paired device may start a session - **Split panes** — the split view renders now, and is reachable from a toolbar button as well as `⌘D`, with `⌘⌥←` / `⌘⌥→` to move between panes and a draggable divider. This line spent a long time under “Not done” for the right reason: the layout code and the chords existed and nothing drew them. - **Updating in place** — on macOS and on Windows both. The app reads the feed and, on your say-so, downloads the release, verifies it and swaps the app, through to the relaunch. The Windows half was exercised on a real PC: an installed build found the next release, pulled it through electron-updater's differential downloader and relaunched itself on the new version. The portable executable remains the one case that cannot be replaced in place, and it says so instead of offering an update it cannot install. ### Not done #### 9 · Ship - **Notarisation, and Windows code signing.** The packaging is done and macOS signing is done — CI signs the `.dmg` with an Apple Developer ID certificate on every tag. What is left is notarisation, which Apple refuses for this account with statusCode 7000, “Team is not yet configured for notarization”, so Gatekeeper still needs the Open Anyway step. There is no Windows certificate either, so SmartScreen still needs the More info override. - **The iPhone client on the App Store.** It is on TestFlight, internal testing only, and no submission has been made. The blocker is honest and specific: a reviewer owns no machine running Terminal Deck, so a pairing screen is a dead end for them. - **Voice dictation.** The app cannot transcribe — no on-device model in this Electron, and the browser speech API starts and then emits nothing. The microphone is off by default and hands over to the operating system's own dictation when switched on. - **A Mac talking to a Windows PC.** Machine-to-machine pairing has been run end to end against the real relay, the real trust store and the real pairing desk — with both ends in one macOS process on loopback, which is where every seam that has broken before lives. Across the internet, between two operating systems, it is the same code and it has not been run. - **The Android client on Google Play.** The signed APK ships with every release and the download page offers it, so there is a build to install. There is no Play listing, which is what would let it update itself. - **Translations.** The app is English only. A Linux window is not on this list because it is not scheduled — electron-builder has no desktop target configured for it. Linux is not absent from the product: the headless host above is what makes a Linux server or a WSL distribution a machine your phone and your desktop can open a session on. Windows does have a desktop target, and every release ships an x64 installer and a portable executable, both built and tested on Windows in CI. ### Known follow-ups Two things are known and not fixed. Both are debts the repository tracks rather than features: - The chunked transcript reader is written three times over — once each for usage, insights and search. All three are tested and working, so lifting them into one helper carries regression risk for no user-facing gain, and it waits. - Escape-to-close on dialogs is correct in code but has never been confirmed by a real keypress, because synthetic key events were not delivered during testing. ### Open questions - The name is provisional. It lives in one file, so changing it is one edit. - How closely the published host should track the desktop. It is on npm and one line installs it, but it is released on its own schedule, so the version the install script gives you can sit behind this release. `terminaldeck --version` says which one you have. --- # About Source: https://terminaldeck.dev/about ## About Terminal Deck is a window for running coding agents. It does not replace Claude Code, Codex CLI or Gemini CLI — it runs the real binaries and puts a workspace around them, so you can see which one is working, which one is stuck, and what the day has moved — and reach any of it from your phone when you are not at the desk. It is free, MIT-licensed, developed in the open, and built by one person. ### Why it exists Running one agent in one terminal tab is fine. Running four is not, and four is where this gets interesting — one refactoring the API, one on the front end, one writing tests, one you started an hour ago and forgot. At that point the terminal stops helping. Tab titles say `zsh`. You cannot tell from outside a tab whether a session is mid-thought or has been sitting on a yes/no question for twenty minutes, so you cycle through them to check — which is the opposite of running things in parallel. You have no idea which one has quietly eaten its context window, or moved ten times the tokens of the others, until something breaks. And when you want to see what an agent actually said three sessions ago, the scrollback is gone. None of that is a modelling problem or a prompting problem. It is a workspace problem, and it is solvable with software that watches the sessions you already run. So: sessions in tabs with a dot that tells the truth about each one; token and context usage read from the transcripts the agent already writes; a browser next to it so the thing being built and the thing building it are one keystroke apart; panels for git, files, artifacts, GitHub, MCP, hooks and readiness, so that checking any of them is not a reason to leave the window; and a phone or browser that attaches to the same sessions from anywhere, with nothing to set up but a six-digit code. ### What it is built to Four rules, applied to code rather than printed on a wall. Each one has a consequence you can see in the app — and a cost, which is the part worth stating. #### Local-first Every number this app shows comes from a file already on your machine. Usage, context, the inspector and session search all read the JSONL transcripts the agent writes; the MCP inspector reads the configuration you already have; the GitHub panel shells out to your own `gh`. Nothing is uploaded, mirrored or indexed elsewhere. The consequence: there is no account, no server and no service to go down. If this project were abandoned tomorrow, the copy on your disk keeps working, because it never depended on anything but your disk. #### No telemetry No analytics, no usage events, no crash reporter, no first-run ping. Two sockets exist and both are named on the privacy page: a loopback endpoint for agent hooks to report to, and an outbound connection to the relay, held open from launch so a paired phone has something to attach to, which carries ciphertext it holds no key for and can be switched off in the Remote panel. The update check fetches a public file on GitHub the way a browser would, with no identifier of any kind attached, and nothing is downloaded or installed unless you press the button. The consequence: the developer has no usage data. Which features are worth keeping is a question that has to be asked out loud, in the issue tracker, rather than inferred from a dashboard. That is a real cost, accepted on purpose. The privacy page spells all of it out. #### Honest empty states A panel with nothing in it should say which of the several possible nothings it is. The GitHub panel distinguishes "`gh` is not installed" from "you are not signed in" from "this folder has no GitHub remote", and shows the command that fixes each. The alerts panel needs positive evidence for every rule it fires, so a brand-new project is silent — a panel that cries wolf on day one is ignored by day two. The consequence: the app is wordier than a prettier one would be, and you never have to guess whether a blank panel means "nothing to show" or "something broke". #### Never claim a capability the code lacks The update button in Settings → Help says what this build can actually do rather than spinning and telling you that you are up to date, and the one artifact that cannot replace itself says so instead of offering. Accounts isolate Claude Code and Codex logins because both were verified against the real CLI; Gemini is listed and refused, with the reason on the row, rather than given a plausible-looking switch that silently shares a login. The app shows no price at all, because the one it could compute would mislead a subscriber and the one that would not cannot be computed. Usage and context are Claude Code only, and say so, because the transcripts they read are Claude Code’s. The consequence: this site is smaller than it could be. Nothing on it describes something the code does not do, and where a feature has a limit, the limit is next to it. ### How it is built Electron, React and TypeScript. The main process owns every terminal process and every piece of filesystem access; a single preload is the only bridge between it and the interface. Sessions are real pseudo-terminals running the real CLI binaries, spawned with your login shell's `PATH`, because a GUI app on macOS otherwise cannot see a `claude` installed by nvm or Homebrew. One working rule shaped more of this codebase than any architectural decision: **check it against something real before writing code that depends on it.** The prompt glyph a CLI draws is `❯` and not `>`. One API request emits many transcript lines with the usage repeated, so the naive sum is wrong. A cache write in a long session is billed at the one-hour rate, not the five-minute one — assume otherwise and the cached portion of a session is under-reported by more than a third. Cookies moved out of the directory the documentation names. Every one of those was discovered by looking, and each would otherwise have shipped as a confident wrong number. The other rule: compiling is not working. A clean typecheck and a silent console have both, more than once, sat behind a window that was visibly wrong. Anything visible gets looked at before it is called done. Every line here was written for this project. Where a feature matches something else on the market, that is because the underlying problem has one sensible answer — no code, components, stylesheets or artwork were taken from anyone. ### Who makes it Terminal Deck is built by **Asad Iqbal**. It started as a tool for his own work — several agents running at once across several projects, and no good way to see what any of them were doing — and it is released because that problem is not unique. It is a single-maintainer project given away for free. There is no company behind it, no support desk and no roadmap commitment; the terms say so plainly rather than implying otherwise. Issues and pull requests are welcome at github.com/asadev/terminaldeck. This project is not affiliated with, endorsed by or sponsored by Anthropic, OpenAI, Google, GitHub or Apple. It runs their tools; it does not speak for them. ### Where it is now Version 0.15.0. The workspace is built; shipping it properly is not finished. **Built and in use** Sessions, status detection, token and context usage, the session inspector, deep session search, git, files, artifacts, quick open and the command palette, GitHub, AI readiness, the dashboard, swarm view, split panes, accounts, hooks, the MCP inspector, the embedded browser and the agent that can drive it, the copilot and its routines, agents you add yourself, the settings window, and remote access from a paired phone, an iPhone, an Android phone or any browser. **Not built yet** Translations, notarisation on macOS, a Windows signing certificate, and voice dictation — the app cannot transcribe, so the microphone is off by default and hands over to the operating system's own dictation. Neither phone client is in an app store: the iPhone one is on TestFlight, internal testing only, and the Android one is a signed APK you download yourself. **Platform** macOS 12 or newer on Apple silicon, or Windows 10 or newer on x64. There is no Intel Mac build — macOS 27 does not run on Intel Macs — and no native ARM Windows build. The Windows one is produced natively in CI and the whole test suite runs on a Windows runner, so a red Windows test stops the release. **Linux** No window, and not nothing. The same core runs as a headless host — plain Node, no Electron in the process, no configuration file — which is what makes a Linux server or a WSL distribution a machine your phone and your desktop can open a session on. One line installs it, and it has been left running in a WSL distribution under a user service holding real agent sessions. **Maturity** Early. The macOS package is signed but not notarised and the Windows package is not signed, so macOS objects the first time you open it and Windows shows a SmartScreen panel. Use it on work that is under version control. --- # Privacy Source: https://terminaldeck.dev/privacy ## Privacy Terminal Deck collects nothing. There is no account, no analytics, no telemetry and no crash reporting. The app makes one network request of its own, and it is not about you: it fetches the public release feed on GitHub to see whether a newer version exists. It also holds a link to a rendezvous relay so a paired phone can reach this machine; that link says only that the machine is online, and it can be switched off. Both are described below. That is a short claim, so the rest of this page is the long version: what is written to your disk and where, what the app reads without copying, and the things that do reach the network — none of which are the app talking about you. Last updated 14 August 2026 · applies to version 0.15.0 ### What is not here Most privacy policies describe data collection. This one has to start by listing the collection that does not happen, because otherwise the rest reads as evasion. - **No account.** There is nothing to sign up for and no sign-in screen anywhere in the app. - **No analytics or telemetry.** No usage events, no feature counters, no anonymous identifiers, no first-run ping. The source contains no analytics SDK of any kind. - **No crash reporting.** Electron's crash reporter is never started. When something goes wrong it is written to a local log file and nowhere else. - **One update check, and it carries nothing.** Twenty seconds after launch, and again when you come back to the window but never more than once an hour, the app fetches the release feed — a public file on GitHub, requested the same way your browser would request it. No identifier, no version history, no machine fingerprint is attached; GitHub sees an IP address and a user agent, as it does for anyone downloading the app. Nothing is downloaded or installed unless you press the button in Settings → Help. - **No credential handling.** There is no API key field. Agent CLIs sign you in themselves, inside the terminal, and keep their credentials where they normally keep them — on macOS, the Keychain. ### What is stored, and where Everything the app writes lives in its own folder under `~/Library/Application Support/`, apart from the hook backups. Settings → Advanced lists the real paths on your machine and opens them in Finder, which is more reliable than a path printed on a web page. **`settings.json`** Everything in the settings window: theme, notification choices, agent defaults, browser preferences, shortcut overrides. **`state.json`** The project folders you have opened and when, your preferences, and the window's size and position. **`profiles.json`, `profiles/`** Your agent accounts — a name, a colour and a configuration directory each — and the configuration directories for the ones the app created. The file keeps its original name on disk. What goes inside those directories is written by the agent CLI, not by this app. **`dashboards/`** Your dashboard widget layouts, one file per project. **`copilot/`, `copilot-layer/`, `copilot-log/`** The copilot's working folder, the instructions and memory it is handed before it starts, and a log of what it did. The last two are outside the folder it can write to, on purpose: an audit log the audited party can rewrite is not an audit log. **`routines/`, `routine-state.json`** The routines you saved and when each last ran. **`remote/`** The devices paired with this machine and the keys that seal the channel to each. Empty until you pair something. **`logs/`** A rotating application log, capped at roughly 1.5 MB in total. It records what the app did — which module failed, how long a call took — not what you or the agent said, and every line is passed through redaction before the Debug panel shows it. **Browser partition** The embedded browser tab's cookies, storage and cache, in a partition folder of their own. See the browser section. **Hook backups** Before the app first edits an agent CLI's settings file, it copies that file to `~/.terminaldeck/hook-backups/`. None of this is synchronised, uploaded or backed up anywhere by the app. Deleting the application-data folder deletes all of it. ### What it reads without copying Several features work by reading files that already exist on your machine. They are read in place; nothing is duplicated into the app's own storage. #### Claude Code transcripts Token and context usage, the session inspector and session search all read the JSONL transcripts Claude Code writes under `~/.claude/projects/` — or under the config directory of whichever profile a session runs as. The app tails those files and keeps totals in memory. It does not copy them, move them, edit them or delete them. #### Your MCP configuration The MCP inspector reads the servers you already configured, at user, project and local scope, rather than keeping a second list of its own. Connecting to a server spawns it as a local process; only stdio servers are dialled from here. #### Agent settings files, when you install hooks Installing hooks writes into Claude, Codex or Gemini's own settings file. Each entry is tagged so only entries this app created are ever removed, the write is atomic, the file's permissions are preserved rather than widened, and a backup is taken before the first edit. Anything the app cannot parse as strict JSON is left alone. #### Your project folders The file tree, viewer, search and git panel read the folders you open. Search is restricted to project roots you have added, files over 2 MB are refused rather than loaded, symlinks that leave the project are refused, and `.deckignore` is honoured by the file tree. ### What leaves your machine The app sends nothing about you. It fetches its own release feed, it dials a relay so a paired phone can reach this machine, and three things running inside or beside it reach the network on their own account. #### The relay a paired phone reaches you through The desktop dials our rendezvous relay at `relay.terminaldeck.dev` at launch and holds the link open, so a phone you have already paired has something to attach to rather than finding a machine that was restarted and never came back. The phone dials out too and the relay staples the two sockets together. Nothing is forwarded, nothing listens for an inbound connection, and there is no other network for you to set up. The switch is in the Remote panel in the sidebar, and switching it off is remembered. An open link on its own moves nothing. The relay learns that a host is online; a device has to have been paired here and approved here before one byte passes between it and this machine. The relay is treated as hostile, because it is a machine on the public internet with every byte passing through it. The two ends run a Noise IK handshake and each payload is sealed under keys derived from material the relay never sees. It learns that a host is online, that some device connected, and how many bytes went each way and when; it cannot read a command, cannot inject a keystroke, and cannot sit in the middle of the handshake without failing to decrypt on the first frame. A file or photo sent from a phone lands in a folder named after the app inside your Downloads, where you can see it. It holds no account and no database. There is no login, because a login would mean a user table, which would mean the relay held something worth stealing. A desktop claims its name by proving it knows a secret the relay only ever hashes, and the mapping from that name to an open socket exists in memory for exactly as long as the socket does. Nothing about it survives a restart of the relay — every desktop simply reconnects. To be precise about what we are not claiming: the relay program keeps no per-connection log, but it sits behind an ordinary HTTPS reverse proxy on a rented server, and we do not represent that no infrastructure anywhere records that a connection happened. The guarantee is about content, and it is structural: the keys are not there, so the stream cannot be read no matter who is looking. #### The agent CLIs A session is the real `claude`, `codex` or `gemini` binary running as a subprocess. When it contacts its provider, it is doing exactly what it would do if you had typed the command in your own terminal. Terminal Deck is not in the path of those requests: there is no proxy, no wrapper and no interception. Your prompts, your code and your files are handled under that provider's policy, not this one. - Claude Code — Anthropic's privacy policy - Codex CLI — OpenAI's privacy policy - Gemini CLI — Google's privacy policy #### Your own `git` and `gh` The git and GitHub panels shell out to the tools you installed and signed in yourself. The app holds no GitHub token and never asks for one; if `gh` is not authenticated, the panel says so and shows the command that fixes it. Nothing about your repositories is stored by the app between runs. #### Pages you open in the browser tab The embedded browser is a browser. Whatever you point it at loads normally and makes whatever requests that page makes. Navigation is restricted to `http` and `https`, downloads are blocked, and every permission request — camera, microphone, location, notifications, clipboard — is refused without asking you, because there is no dialog here to ask with. One thing worth knowing before you type: **text in the address bar that is not a URL becomes a web search**, sent to DuckDuckGo. A bare hostname or a `host:port` is treated as an address and stays local. **The one listener.** When agent hooks are installed, the app opens a small HTTP server so those hooks have somewhere to report. It binds to `127.0.0.1` on a port the operating system picks, requires a token generated fresh on every launch and never written to disk by the app, and refuses any request whose `Host` header is not a loopback address. Nothing outside your machine can reach it. The security page describes it in full, including what that token is and is not worth. ### The embedded browser's data The browser tab has its own cookie jar, kept apart from everything else the app stores. It persists between launches on purpose — a dev server behind a login should not need signing into again every morning. Turn off "keep cookies and logins" in Settings → Browser and the partition's cookies, storage and cache are cleared when you quit. A tab can also be marked isolated. An isolated tab gets a partition of its own that exists only in memory: nothing is written to disk for it, it shares no cookies with the other tabs, and its data is gone when the tab closes or the app exits. That is what makes signing into the same dev app as a second user possible. **Cookie import is the one place this app reads another program's secrets.** If you ask it to, it can copy cookies from an installed Chromium browser into its own partition so you do not have to sign in twice. It happens only when you start it, macOS prompts for keychain access naming the app, and a denied or unanswered prompt is reported as exactly that rather than as an empty profile. Cookie values never appear in a log, an error message or anything sent to the interface — the record kept so you can later remove them holds only the name, domain, path and secure flag. Chrome's own profile is opened read-only, through a temporary copy, and is never written to. ### Diagnostics you choose to share The app can produce a support bundle: version numbers, which agent CLIs it detected, where it keeps its files, which parts of the app wired themselves up, and the tail of the local log. It is generated only when you ask for it, copied to your clipboard, and sent nowhere. What you do with it afterwards is up to you. Because a bundle exists to be pasted somewhere public, everything in it is passed through redaction first, and it reports how many substitutions were made. IPC call arguments are never recorded at all — the timings are useful, and the arguments are where your prompts and file contents would be. Environment variables are listed by name; the only values included are a short allowlist that carries no secrets. Redaction is careful but it is still software: read a bundle before you post it. ### This website terminaldeck.dev runs no analytics and makes no request to any other host — the two typefaces are served from this domain, and there is no embedded video, no comment widget, no tag manager and no font CDN. There is nothing here to track you with, which is why you were not asked to consent to anything. **Cookies.** Reading this site sets none. Signing in to Commons sets two, and only those two. `td_session` is your session: HttpOnly so no script on the page can read it, Secure, SameSite=Lax, and its contents sealed with AES-256-GCM so a copy taken off the wire is unreadable without the server’s secret. `td_in` beside it is a plain flag with one job — telling the page which header to draw before the first paint — and it carries no identity and grants nothing. Signing out clears both, and neither is used for advertising or measurement. The site is hosted on Vercel, and Commons is a database and a set of small functions run for it. Those see the ordinary request information any web server sees. That is between you and them; it is not collected, joined to anything, or used to build a picture of you. ### The rest #### Children Terminal Deck is a developer tool for running coding agents and is not directed at children. It collects no personal information from anyone, so there is nothing collected from a child either. The agent CLIs it runs have their own age requirements; those apply as they normally would. #### Changes to this policy If the app ever starts collecting something, this page changes first and the change is visible in the repository's history before the release that makes it true. The date at the top is the last edit. There is no mailing list to notify you, so a version that added any kind of data collection would say so in its release notes as well. #### Contact Questions about this page, or a claim on it you think is wrong, belong in the repository: the issue tracker. The source is public, so every statement here can be checked against the code rather than taken on trust — if a claim does not match the code, the claim is the bug. Suspected vulnerabilities should go through the security page instead of a public issue. --- # Terms Source: https://terminaldeck.dev/terms ## Terms Terminal Deck is free software under the MIT licence. You may use it, copy it, change it and ship it in your own products. It comes with no warranty of any kind, and what your agents do on your machine is your responsibility. These terms are written in plain language. Where they and the MIT licence text differ, the licence in the repository is the one that governs. Last updated 14 August 2026 · applies to version 0.15.0 ### The licence The app is licensed under the MIT licence, as recorded in the project's `package.json` and published in the source repository. In practice that means you may use it commercially, modify it, distribute it and include it in closed-source work, provided the copyright notice and the licence text travel with any substantial portion of the code you redistribute. There is no separate commercial licence, no per-seat term and nothing to buy. Nothing on this site creates a subscription or an ongoing obligation in either direction. ### No warranty. Provided as-is. The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and non-infringement. It is not certified, audited or guaranteed for any use, and no promise is made that it is free of defects, that it will keep working, or that any particular version will ever be released. This is an early build. The macOS package is signed with an Apple Developer ID certificate but not notarised, and the Windows package is not signed at all; the macOS build runs only on Apple silicon and the Windows build only on x64, and nothing is ever pushed to you: the app can check for a new version and install one you accept, on macOS and on Windows both, and the one build that cannot replace itself — the portable Windows executable — says so instead of offering. **Back up your work.** This app runs coding agents that edit files. Use it on projects under version control, with a clean working tree you can return to. That is good practice with any agent, and it is the only recovery mechanism either the agent or this app gives you. ### You are responsible for what the agents do This is the clause that actually matters, so it gets its own section rather than a line in a list. Terminal Deck spawns real agent CLIs as subprocesses on your machine, with your user account's permissions and your login shell's `PATH`. It does not sandbox them, does not confine them to the project folder, does not review or filter their actions, and does not ask you to confirm anything they do. Whatever those agents can do in your own terminal, they can do here — including in a session you are not currently looking at. Anything an agent writes, deletes, commits, pushes, installs, publishes or spends is your responsibility. Any permission you grant, hook you install, MCP server you connect or command you let an agent run is your decision. So is running an agent in a folder you cannot afford to lose. You are also responsible for using it lawfully, and for the code you produce with it — including whether that code may be used where you intend to use it. ### Third-party software and its terms Terminal Deck runs programs it does not ship, does not bundle and does not modify. Each one is yours, installed by you, signed in by you, and governed by its own terms and pricing — not by these. - **Claude Code** — Anthropic's terms and usage policies apply to your account and your usage. - **Codex CLI** — OpenAI's terms apply. - **Gemini CLI** — Google's terms apply. - **`git`, the `gh` CLI, and any MCP server you configure** — their own licences apply, and any repository or service they reach is governed by that service's agreement with you. Any charge from those providers is between you and them. Terminal Deck cannot stop an agent from spending money and does not try to. #### There are no cost figures, and the usage figures are a reading This app shows you no prices. It used to: token counts multiplied against a built-in table of published list prices. That number was arithmetic and it still misled anyone on a subscription, where a flat monthly fee does not decompose into a per-session amount — and no honest subscription figure could replace it, because no plan publishes a token allowance or a per-token value to compute one from. Both are gone. Nothing on any screen is a statement about money you owe. What is shown instead is token counts, cache shares and context-window occupancy, read out of the transcripts the agent wrote on your own disk. Treat those as a reading of those files, not as an account statement: sessions are attributed by the day they were active, a session that began yesterday is counted in full, and a plan limit is only ever repeated back from what the agent CLI itself printed. Your provider's own billing is the only truth about what you are charged. ### Limits of liability To the fullest extent permitted by law, the author is not liable for any claim, damages or other liability arising from the software or its use — including lost or damaged data, lost work, lost profits, service interruption, costs incurred with an agent provider, or anything an agent did while running inside this app — whether in an action of contract, tort or otherwise, and whether or not the possibility of such damage had been raised. Some jurisdictions do not allow certain warranties to be disclaimed or certain liabilities to be excluded. Where that is the case, the disclaimers above apply to the maximum extent that jurisdiction permits, and nothing here removes a right you cannot legally waive. ### Support, changes and the rest #### There is no support obligation This is a project maintained by one person and given away. Issues and pull requests are welcome at the issue tracker, and nothing on this site promises a reply, a fix, a timeline or a future release. #### Changes to these terms These terms may change. The date at the top is the last edit, and every version is in the repository's history. A change applies to the version of the app you download after it; it cannot reach back and alter the licence under which you already received a copy, because that licence came with the copy. #### Trademarks and names Claude, Claude Code, Codex, Gemini, GitHub and macOS are the marks of their respective owners, used here only to say truthfully what this app works with. This project is not affiliated with, endorsed by or sponsored by Anthropic, OpenAI, Google, GitHub or Apple. #### Privacy and security The app collects nothing, and the privacy page sets out exactly what is stored on your machine and what reaches the network. The security posture, its stated limits, and how to report a vulnerability are on the security page. --- # Security Source: https://terminaldeck.dev/security ## Security Terminal Deck runs untrusted things on purpose: agent CLIs that write to your files, and web pages loaded into a browser inside the app window. This page says what is done about that, and what deliberately is not. Every claim below is a property of the code, checkable in the public source. Where a defence has a known limit, the limit is stated next to it — a security page that lists only its wins is not much use to the person deciding whether to trust it. Last updated 14 August 2026 · applies to version 0.15.0 ### The threat model, stated first This is a local desktop app with no account and no multi-user surface, and it stores nothing of yours on anyone else's machine. Remote access is the one thing that puts a third party anywhere near your work — the app holds a link to a rendezvous relay from launch, so a paired phone has something to attach to — and it is built so that having that link open is not a decision about trusting the relay. What is left is worth being precise about. - **Defended:** a hostile web page loaded in the embedded browser tab; a process on your machine poking at the local hook endpoint; a secret leaking out through a support bundle or the debug log; the app being talked into reading or deleting something outside the folder it was pointed at; the relay that carries a remote session, which is treated as hostile and cannot read a byte of what passes through it — every payload is sealed under keys established between the desktop and one paired phone, from material the relay never sees. - **Not defended, by design:** the agents themselves. Terminal Deck runs them with your permissions and does not confine, filter or review what they do — see below. - **Out of scope:** an attacker who is already running code as your user account. At that point they can read the same config files and trigger the same keychain prompts the app can, and nothing in a userland app changes that. ### The application window The standard Electron hardening, plus the one deviation, named rather than omitted. #### Context isolation on, node integration off The interface runs with `contextIsolation: true` and `nodeIntegration: false`. Interface code has no `require`, no `process` and no filesystem. A single preload is the only bridge, and it exposes explicit named methods — `ipcRenderer` itself is never handed to page code, so there is no channel to call that the bridge did not deliberately publish. #### A strict CSP in production The policy is set from the main process as a response header rather than a `` tag, so it cannot be edited out of the markup. In a production build it is `default-src 'self'; script-src 'self'; connect-src 'self'`, with fonts and images limited to `'self'` and `data:`. Two honest details. `style-src` permits inline styles, which the interface needs. And the development build is deliberately looser — Vite's dev server injects an inline module preamble that a strict `script-src` blocks, which renders the window blank — so `'unsafe-inline'` and `'unsafe-eval'` apply only when you are running from source. #### Links leave the app Any attempt to open a new window from the interface is denied, and the URL is handed to your real browser instead. Nothing navigates the application window away from the app's own interface. #### The deviation: the renderer is not sandboxed The main window runs with `sandbox: false`. Context isolation still separates the preload's world from page code, and the preload uses no Node APIs — but the Chromium sandbox is not applied to the app's own renderer, and that is worth knowing rather than discovering. The embedded browser's guest views, which load pages nobody vetted, do run sandboxed. ### The embedded browser is treated as hostile A page you load is untrusted whether it is your own dev server or something you followed a link to. It is given its own process, its own session and no route inward. Guest pages are loaded into a separate `WebContentsView` — a distinct process with its own session, not an iframe sharing the app's renderer. The view is created with `sandbox: true`, `contextIsolation: true`, `nodeIntegration: false`, `nodeIntegrationInSubFrames: false`, `webSecurity: true`, `allowRunningInsecureContent: false` and `webviewTag: false`. **Navigation** Allow-listed to `http:` and `https:` — not deny-listed. `file:` is refused at every entry point: the URL bar, link clicks, redirects and subframe navigations. A page that could reach `file:///` could read your disk through the very inspection channel the feature exists to provide, so each of those paths is guarded separately. **Permissions** Every permission request and permission check is refused outright — camera, microphone, location, notifications, clipboard, the lot. There is no prompt, because there is no interface here to prompt with and a silent yes would be worse. **Downloads** Blocked. A page cannot put a file on your disk through this browser. **Pop-ups** `window.open` and `target="_blank"` get no window; the attempt is reported in the tab's error line instead. **The inspect channel** The script running inside the guest page only reports facts about a clicked element — tag name, id, whether that id is unique, position among siblings. It exposes nothing over a context bridge, so page code has no route to any IPC channel. Every decision — which selector wins, what gets escaped, what is allowed to reach a terminal — is made in the main process against a closed list of attributes, where it is tested against adversarial input. The URL attached to a capture is the one the main process knows, never the page's own claim about itself. **Isolated tabs** A tab can be given a partition of its own that exists only in memory and is cleared when the tab closes. The partition name is validated against a strict pattern before use, so a malformed value cannot quietly create an on-disk partition instead. An isolated tab is hardened identically to a shared one — isolation is not a softer mode. ### The local hook endpoint The app's only listening socket. It exists so agent hooks have somewhere to report instead of the app scraping terminal output. Three properties do the work, in this order. - **It binds to `127.0.0.1` only**, on a port the operating system assigns. Nothing off your machine can reach it — that is the boundary that actually matters, and it does not depend on the two below being perfect. - **Every request must carry a per-run token**, compared in constant time — including the length check, so a wrong length does not fail faster than wrong bytes. The token is generated at launch and never written to disk by the app, so a hook left behind by a previous run cannot post into this one. It is deliberately not exposed to the interface: a secret that reaches page code is a secret one bug away from leaving. - **The `Host` header must be a loopback literal.** A hostile page in any browser on your machine can be pointed at `127.0.0.1` by DNS rebinding. It could not guess the token anyway, but refusing a rebound host costs nothing. Bodies are capped at 1 MB and drained rather than cut off, so an oversized upload is answered instead of hanging. Header and request deadlines are seconds rather than Node's minutes. The response is always an empty `204` — anything returned would be parsed by the CLI as hook output and could steer the agent, and this endpoint observes rather than steers. The token is forgotten at shutdown. **What the token is not worth.** It lives in the agent CLI's own settings file, which for two of the three providers is mode `0644` on this platform. Another process running as your user can read it. The token stops confused software and drive-by requests from a browser; it is not a defence against a local attacker who is already reading your home directory. The loopback bind is the boundary that is. ### Secrets are redacted before they can leave A support bundle exists to be pasted into an issue, which makes it the single most likely place for a credential to escape. #### Four layers, in order First, literal values already known to be secret — anything secret-looking read out of the environment. Then structure: PEM blocks, credentials embedded in URLs, `Authorization` and `Cookie` headers, and assignments whose key name says the value is a secret. Then known token shapes by issuer prefix. Then an entropy sweep for long, dense, mixed strings carrying no other signal. Home directories and usernames are folded away last. They are not secrets, but a bundle that names you is more personal than it needs to be. #### It errs towards losing information A 40-character lowercase hex string is redacted, which means commit SHAs are collateral damage — a legacy GitHub token has exactly that shape and nothing in the string tells them apart. A missing SHA costs one follow-up question; a leaked token costs an account. The bundle reports how many substitutions it made, so a suspiciously low number is visible rather than silent. #### Two things are never recorded at all IPC call arguments are never captured. Which channel was called and how long it took is useful; the arguments are where your prompts, file contents and tokens are, and a debug panel showing them would be a leak with a table around it. Environment variables are reported by name only, except a short allowlist (`PATH`, `SHELL`, `TERM`, `LANG`) that carries no secrets. Knowing `GITHUB_TOKEN` is set is useful; knowing its value is not. #### The log too The rotating application log is capped and every line is passed through the same redaction before the Debug panel displays it or a bundle carries it. Redaction is careful, not magic — read a bundle before you post it. ### Pairing, stated as arithmetic A pairing code is six digits. That is a smaller number than it used to be, so here is the whole of what makes it sound rather than an assurance that it is. Six decimal digits is 106 — a million codes. The format it replaced was eight Crockford base32 characters, which was 328, about 1.1 trillion. That is a reduction by a factor of roughly 1,099,511, and no wording makes it smaller. It is acceptable because of what a guess has to survive, and each of the following is pinned by a test that fails if somebody removes it: - The code lives **sixty seconds**, and is dead at the boundary. - It is **single-use**, and is burned the instant it matches — before the device name is checked and before anything is written to disk. - **Five wrong answers kill the code**, not the guesser. The counter is against the one code on screen, so an attacker who dials from a fresh address or mints a fresh key for every attempt does not get a fresh budget. That counter is what carries this format. Five guesses against 106 is 5 × 10−6 — one in two hundred thousand, per pairing window. The old format was 4.5 × 10−12. The number is written here rather than buried, because one in two hundred thousand is a figure somebody should be allowed to disagree with. **The code has never been the gate.** What redeeming one buys is a pending device — a row in a list on the other machine that a human has to approve before it can attach to anything. It is the first of two doors, and it is the half that is only worth as much as the sixty seconds and the five tries. A typed code cannot carry an address, so it names a rendezvous slot at the relay rather than the machine itself. That lookup would be a free oracle if it were cheap — an attacker who could enumerate slots would sweep the million, find the live one and learn the code exactly — so the slot is derived through memory-hard scrypt. The responder's key pair for that offer is derived from the code as well, so a hostile relay cannot answer in the machine's place. There is no QR code and no pairing link. Both were removed rather than hidden: the QR did not work, and the link was a two-hundred-character string carrying a live bearer secret that had to travel through a messaging app to be useful — which is a pairing token somebody else's server then holds a copy of. Every route into a credential write that they used is gone, along with the camera permissions the scanners needed. Nothing is stored that could become a device. Paired devices are recorded as scrypt hashes and public keys only, so a stolen credential file cannot be replayed into a pairing. The one new secret on disk is this machine's own static private key, written `0600`. ### Filesystem and configuration **Search scope** Project search only runs against roots you have explicitly added, so a folder that merely looks like a project cannot be enumerated over IPC. **File reads** Files over 2 MB are refused rather than loaded, symlinks that resolve outside the project are refused, and directory loops are detected instead of walked. **Deleting an account** Two independent guards. The directory must be one the app itself created inside its own storage, and it must not be your home directory, an agent CLI's real configuration directory or the filesystem root — the second check exists precisely for a hand-edited or corrupted config file. An account pointing at a directory you already had is removed from the list, and its files are left alone. **Editing agent settings** Hook entries the app writes are tagged, and only tagged entries are ever removed — a machine that also runs another tool's hooks keeps them. Writes are atomic, the file's original permissions are preserved rather than widened, a backup is taken before the first edit, and any file that does not parse as strict JSON is left untouched. **MCP servers** An MCP server is an arbitrary program you configured. Each connection is a separate process with a wall-clock timeout on every call, so a server that fails to spawn, spawns and never speaks, or dies mid-listing cannot hang the app. **Chrome cookie import** Chrome's profile is opened read-only through a temporary copy and never written to. The keychain prompt macOS shows names this app; denying it is reported as a denial, not as an empty profile. Cookie values never enter a log, an error message or an IPC payload. ### What is deliberately not defended **The agents are not sandboxed.** A session is the real CLI running as a subprocess with your account's permissions and your login shell's `PATH`. Terminal Deck does not confine it to the project folder, does not review its tool calls, does not filter its file writes and does not add a confirmation step. Whatever an agent can do in your terminal, it can do here — including in a session you are not currently looking at. That is the honest shape of the product rather than a gap to be closed in a patch. The app's job is to show you what your agents are doing, not to stand between them and your disk. Run them on projects under version control, and grant permissions the way you would at a bare prompt. **Also true today:** the macOS build is signed with an Apple Developer ID certificate but not notarised, and the Windows build is not signed at all, so Gatekeeper will object on macOS and SmartScreen will object on Windows, and you will have to open each one explicitly the first time. Nothing is ever pushed to you: on macOS the app can check for a new version and install one you accept, but on Windows that path has never been exercised and a portable executable cannot be replaced in place at all. So a security fix reaches you when you come and get it — from this site or from the repository. ### Reporting a vulnerability Please report privately first, and give it time to be fixed before writing about it publicly. - Open a private security advisory on the repository. That keeps the report visible only to you and the maintainer until there is a fix. - If advisories are unavailable to you, open a normal issue that says only "security report, please make contact" — with no details in it — and the conversation can move somewhere private. What helps, in rough order of usefulness: - The version, and whether it is a packaged build or run from source. - What an attacker gains, in one sentence. - Steps to reproduce, or a small proof of concept. - Any suggested fix, if one is obvious to you. This is a project maintained by one person, so no response time is promised and there is no bug bounty. What is promised: a genuine report gets a genuine reply, the fix is described honestly in the release notes rather than folded silently into a "maintenance" line, and credit goes to you unless you would rather it did not. Please do not use this issue tracker to report a vulnerability in Claude Code, Codex CLI, Gemini CLI or any other program this app runs. Those go to their own vendors — nothing here can fix them.