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.21.4
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 app 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
Native screens on both systems, and the few web pages inside them held to a closed set of rules.
Native screens, one bundled page
On the Mac the window is SwiftUI and AppKit: terminals, the browser, files, git, agents, hooks, MCP, the Store, tasks, Hoot and most of Settings are native screens. The few screens that are not yet native come from one page bundled inside the app, shown in WebKit.
That page reaches the app two ways only: one named message handler, accepted from
the main frame and the app’s own origin, and a loopback address on
127.0.0.1 that answers only requests carrying a token made for this
run.
A strict Content Security Policy (CSP — a rule the app sets that stops its page loading anything from outside itself)
The bundled page is served with
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'.
It permits inline styles, which the page needs, and nothing from anywhere else.
Links leave the app
An http, https or mailto link from the
bundled page opens in your default browser. Any other attempt to open a window is
blocked, so nothing navigates the app’s own window away from the app.
Signed, hardened, not sandboxed
The Mac app is signed with a Developer ID and runs with the hardened runtime. It is not in the App Sandbox, because it runs your terminals and agents, which need your files and tools. Its entitlements are the microphone, for dictation, and an app group shared with its widgets; the helper that runs plugin JavaScript has only the one it needs to compile JavaScript. The widget extension is sandboxed.
One honest detail: the bundled page can be inspected with Safari’s Web Inspector.
On Windows
The window is WinUI 3. Terminals are drawn in WebView2 with developer tools off,
no host objects, new windows swallowed, navigation pinned to the bundled terminal
page and every permission request denied. A previewed artifact runs with scripts
off and default-src 'none'.
The embedded browser
A page you load is untrusted whether it is your own dev server or something you followed a link to.
On the Mac each tab is its own WebKit view, so the page runs in WebKit’s own web-content process rather than inside the app. Each browser profile keeps its own data store; an isolated tab keeps its data in memory only.
- Addresses
-
A tab loads
http:andhttps:pages, plusabout:,data:andblob:content.file:andjavascript:are refused. Other links, such asmailto:, are handed to macOS, and only when you click them. - Permissions
- The app holds no camera or location permission, so it has none to give a page.
- Downloads
- Allowed, through the app’s own download manager, into a folder you choose.
- Pop-ups
- Opened only from a click. A pop-up becomes a tab of its own that shares the opener’s data, so sign-in windows keep working.
- Agents
- An agent drives a page only through the app’s own browser tools — open, read, step, screenshot, network, hand back to you.
- Isolated tabs
- An isolated tab shares no cookies with other tabs, writes nothing to disk, and its data is gone when the tab closes.
- On Windows
-
The browser is WebView2. Isolated tabs are InPrivate, pop-ups become tabs for
httpandhttpsonly, and developer tools are available from the browser’s own menu.
The local hook endpoint
It exists so agent hooks have somewhere to report instead of the app guessing from terminal output alone.
On the Mac it is not a network port at all.
-
It is a Unix socket in the app’s own folder, readable only by
you (
0600inside a0700folder), and the process that connects must run as your user. Nothing off your machine can reach it. -
Every request must carry a token made fresh for each run, 24 random
bytes, compared in constant time. The hooks read it from a small file in the
same folder, also
0600, which is deleted when the app stops. -
The
Hostheader must belocalhost,127.0.0.1or::1.
Headers are capped at 64 KB and a body at 1 MiB. A wrong token or host is
answered with 403.
On Windows it is a port on 127.0.0.1 that the system picks. Every
request carries a secret compared in constant time, a request with an
Origin header is refused, and bodies are limited to 64 KB.
What the token is not worth. Another process running as your user
can read a 0600 file. The token stops confused software and drive-by
requests; it is not a defence against a local attacker who is already reading your
home folder.
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.
What is replaced
On the Mac, Settings › Advanced › Diagnostics collects a bundle and copies it; nothing is sent anywhere. Before that, secret-looking environment values, private keys, credentials inside URLs, authorization and cookie headers, secret key-value pairs and bearer tokens, known token shapes and long random-looking strings are replaced.
Your home folder is folded to ~ and your user name replaced. They are
not secrets, but a bundle that names you is more personal than it needs to be.
The logs
The rotating app log is passed through the same redaction before it is written,
and written readable only by you. The app’s event log,
engine.log, is written as it is. 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
- Who reaches which folder
- Your own window reaches your files. Everything else is held to less: a paired device to the folders you granted it, an agent session to its own project folder, and Hoot to the open project folders and your home folder.
- File reads
- Files over 2 MB are refused rather than loaded. Paths are opened one step at a time without following links, so a symbolic link that leaves the project is refused, and a search never follows links and stops twelve folders deep.
- Deleting an account
-
Files are removed only from a folder the app manages, and never from
/, your home folder or an agent CLI’s own configuration folder, whatever a settings file says. The Mac’s own system account cannot be removed. - Editing agent settings
- Hook entries the app writes carry a marker, and only marked entries are ever removed, so another tool’s hooks are kept. Writes are atomic, the file’s permissions are kept, a backup is taken before the first edit, and a file that is not valid JSON is left untouched.
- MCP servers
- An MCP server is a program you configured. Each one runs as its own process in its own process group, so closing it ends everything it started, and every call has a time limit, so a server that never answers cannot hang the app.
- Cookie import
- The Mac app does not read another browser’s cookies. On Windows, cookies can be imported only from a file you exported yourself.
What is deliberately not defended
The agents are not sandboxed. A session you start on your own
machine is the real CLI running 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. The exception is a session started from someone else’s
paired device: on the Mac it runs in a macOS sandbox limited to the folders you
granted that device, with a home folder and agent accounts of its own.
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 app is signed with an Apple Developer ID certificate but not notarised, so Gatekeeper objects the first time you open it, and the Windows package is signed with the project’s own certificate, which you have to trust once before Windows installs it. Both apps show a new release in a card just above Settings. The Mac installs it only when you say so; the Windows app installs it when you click, or by itself once nobody has used the PC for ten minutes and no AI is working. Either way the app checks the release’s signature and the file before it replaces anything.
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.