What "runs on Linux" means for TST Desk, where the semantics genuinely differ from macOS and Windows, and which of those differences are decisions rather than gaps. Written for someone on an X11 or Wayland session wondering whether a refusal is the product or their desktop.
Most of the daemon is platform-neutral. The places it is not are the same shape as on Windows: an operating-system concept that has no equivalent elsewhere.
The product data directory on Linux / BSD is $XDG_DATA_HOME/tst-desk, or
~/.local/share/tst-desk when that variable is unset. That is the path
docs/configuration.md documents and the path both the host and
user_data_dir() now join.
The Tauri identifier com.thatsimpletech.tstdesk is the bundle id and the
keychain service name. It is not the XDG directory name. A host that joined
the identifier onto dirs::data_dir() stored GUI sessions and port.json in
~/.local/share/com.thatsimpletech.tstdesk while tst run / tstd used
tst-desk. Those two trees are now the same leaf.
If only the reverse-DNS leftover exists, it is renamed once to tst-desk. If
both exist, tst-desk wins and the leftover is left alone — two live stores
are not merged.
--data-dir moves the session store, the audit database, logs/tstd.log, and
config.yaml. With no flag, config.yaml stays in the platform directory above.
The OS keychain does not move.
API keys go through secret-tool (libsecret) over stdin. The service is
com.thatsimpletech.tstdesk. Install libsecret-tools and have a Secret
Service running (GNOME Keyring, KWallet, or the desktop's equivalent).
A locked collection is keychain_locked, not raw CLI stderr. Unlock the login
keyring, then retry Store. There is no Keychain Access app on Linux.
secret-tool is discovered at call time. The Linux backend is selected
whenever sys.platform is Linux, even if the binary is missing — the first
store or lookup is where that shows up. A missing binary is a typed
KeychainError ("install libsecret-tools"), not a crash: setup_state
probes every named credential, and a clean guest without libsecret must
still handshake and run the keyless local preset.
sessions.json, port.json, and remote-token are written chmod 0600.
Unlike Windows, that is a real restriction here. The containing XDG directory
is usually 0700 as well.
Live desktop computer-use is X11 only (TD-2001). mcp/tst-cu-mcp talks to
libX11 / libXrandr / libXtst and captures through Pillow. Policy
(kill-switch, expect_window) is the same layer as macOS and Windows.
health decides support from the environment, without opening a Display:
| Session | health.supported |
session_type |
|---|---|---|
Native X11 (XDG_SESSION_TYPE=x11) |
true |
x11 |
| Wayland | false |
wayland |
XWayland DISPLAY on a Wayland session |
false |
wayland |
X11 has no TCC-style gate. check_permissions says so (no_gate) and names
the limits that actually bite: a missing DISPLAY, a missing XTEST
extension, or a Wayland session. There is nothing to grant in Settings.
Wayland cannot be served by extending the X11 backend (TD-2002). Capture
would be portal ScreenCast over PipeWire; input would be portal RemoteDesktop
or libei; foreground_window is not compositor-neutral. Until a later epic,
the product refuses rather than driving XWayland clients and ignoring native
Wayland apps. expect_window never degrades: if foreground cannot be read,
actuation is refused.
The real-display rust ring is the same session-scoped signal as macOS and Windows (TD-3407): it lights on the first computer-use tool of a turn and stays until turn end, cancel, or the kill-switch. Wayland has no ring. Desktop Design-mode hit-test (TD-3406) is observe-only: AT-SPI when the library is present, otherwise the EWMH window under the point on X11. Wayland still has no Design AX path.
The browser computer-use path (Playwright) is not X11-specific.
Closing the window is not quitting the app (TD-2902).
- Close (title-bar X) hides the window.
tstdkeeps running. - Quit (menu / palette Quit TST Desk) sends
shutdownand reaps the process group.
The host signals that group (SIGTERM, then SIGKILL past the grace) with the
kill system call, not the kill command. The procps kill in Ubuntu 24.04
misreads kill -KILL -1234: the signal went to the wrong group, so tstd
outlived Quit, or to every process the user owns when the pid started with 1.
Builds before this fix (TD-1406) did that on 24.04; 26.04's procps is patched.
A second launch never starts a second host (TD-5024): it hands over to the
running one over the D-Bus session bus, which shows its window, and exits.
With Keep running on, closing every window in any order leaves the host in
the tray (TD-5020); tray Show or a second launch opens a new window. The
tray (TD-4703) is Show / New window / Quit; the tooltip counts running
sessions. There is no macOS Reopen event on Linux.
The host keeps two logs next to tstd.log: logs/tstd.stderr.log (tstd's
raw stderr, owner-only, one .1 kept) and, in release builds,
logs/host.log (its own lifecycle: start, spawn, attach, restart, quit; one
dated archive).
While hidden, a running session or a parked approval updates the window title (the cheap Linux / Windows badge path). OS notifications still fire.
Let open turns finish first, so a restart does not cut one (TD-5023):
cd core && uv run tst drain --timeout 120- Replace the AppImage.
- Quit TST Desk (tray or menu Quit), then launch the new AppImage. Launching it while the old host runs only raises the old window (TD-5024), so the old build would keep running.
tst drain waits up to --timeout seconds for open turns, then keeps
refusing new ones for --hold seconds (default 120) so the restart lands
on a quiet daemon. It exits 0 when no turn is still running, 2 when one is,
and 1 when no daemon is running; it never starts one. It prints only
session ids, each with how it waits: running, paused, waiting on the user (the progress guard or a cap paused it) or waiting for approval.
A paused turn or one parked on an approval never finishes by itself, so by
default it does not hold the deploy up (TD-5155): tst drain lists it
under "Waiting on the user" and still exits 0. The restart seals it as
"Cut off by a restart" with a Continue chip (TD-5018), and any message the
daemon had accepted for that session comes back held above the composer,
with Send and Remove; nothing is sent by itself. Attached images do not
survive the restart: a held message keeps its text and says how many
images it lost. Pass --strict to exit 2 for paused and approval turns
too, when you would rather wait for the person.
The hold refuses only verbs that start a turn. Continue (resume),
Approve and Deny still pass, so a person who clicks one between tst drain
returning 0 and the stop sets that turn running again, and the restart
then cuts it. Nothing is lost (TD-5018 seals the cut turn and offers
Continue), but the turn has to be picked up again. Stop the daemon promptly
after tst drain returns, or use --strict when someone may be at the
window (TD-5158).
The restarted daemon starts without a hold; tst drain --release lifts it
if you call the deploy off. A turn cut anyway is closed as "Not finished"
when the daemon comes back (TD-5018), and a second tstd on the same data
dir waits up to 10 s for the first to exit before giving up (TD-5022).
- Loopback binding.
127.0.0.1on every platform. Prime directive §2.1 has no platform clause. - Steering-file writes.
AGENTS.md,CLAUDE.mdand.tst/rules/**are refused identically everywhere. - Manifest and config paths. Written with forward slashes regardless of host.
- Killing a command. POSIX
setsid+killpgis the reference implementation. Windows is the special case (docs/windows.md§4).
The README Development section lists the apt packages. On Ubuntu 26.04 use
libayatana-appindicator3-dev, not libappindicator3-dev: the older name
removes network-manager-applet.
For where each of these decisions was made, see the TD-2001, TD-2002, and
Linux data-dir entries in DECISIONS.md.
This host has no /dev/kvm, so the Linux "clean VM" is Docker
(ubuntu:22.04 for the extracted sidecar, ubuntu:24.04 for dpkg -i
plus the windowed host). From a built .deb / AppImage:
core/scripts/smoke_linux_bundle.sh [path-to-deb] [path-to-appimage]
That script proves: no system Python on the sidecar (ldd + empty
PATH), dpkg -i + xvfb-run tst-desk writes port.json, a protocol
turn against the shipped local preset (loopback mock → fs_write →
reply), and AppImage --appimage-extract of the same sidecar. It does
not tick the four-platform packaging boxes. macOS and Windows guests use
core/scripts/smoke_macos_bundle.sh and core/scripts/smoke_windows_bundle.ps1
(TD-4906). GitHub Actions package.yml is a separate gate.