Persistent sessions (daemon mode)
Experimental
Daemon mode is new in 0.7.0 and still settling. It is built into every Contour and covered by the test suite, but its command-line flags and its wire protocol may change between releases.
Contour already gives you tabs and split panes inside one window. Daemon mode is different: it moves your sessions into a background process, so they survive the window that shows them. Close the window, reattach later, or attach from another machine — the shells keep running either way.
Terms. The daemon is the background process. A workspace is what it holds: a set of tabs, each an arrangement of panes. A window is an operating-system window of the Contour GUI, which shows one workspace; closing the window leaves the workspace and its shells with the daemon.
That buys you three things:
- A long build, a database migration or a training run keeps going after you close the window — deliberately or by accident.
- Work done over SSH survives the link dropping. Reconnect, reattach, carry on.
- The same sessions are reachable from a second machine, over an encrypted connection.
If you have used tmux or screen, this is the same idea, with two differences: the sessions are
rendered by a real Contour window (images, hyperlinks, sized text and all), and Contour can speak
tmux's own protocol in both directions — so it interoperates with tmux rather than replacing it.
Quick start
That is the whole thing. contour client looks for a daemon, starts one if none is running,
and opens a window showing its sessions. Close the window and the shells stay alive; run
contour client again to pick them back up — with their scrollback, so you can scroll back through
what the session did while nothing was watching.
To manage the daemon yourself, start it first:
contour daemon & # runs in the FOREGROUND; background it or use a service manager
contour client # attaches to the daemon you started
The distinction matters, because the two daemons have different lifetimes:
| How it started | When it exits |
|---|---|
Auto-started by contour client |
10 seconds after it has no sessions and no clients — it belongs to the clients that needed it, so nothing is left behind |
Started by you (contour daemon) |
Never on its own. It keeps serving with zero sessions, ready for the next client |
Started by the OS (contour daemon-service) |
Never on its own, see Starting the daemon at login |
Closing the last window therefore keeps your sessions: the auto-started daemon stays up as long as any session lives, and for 10 seconds after the last one is gone and the last client has disconnected. A client that attaches within those 10 seconds keeps the daemon alive.
The policy is the daemon's --lifecycle option, and it rides on the daemon, not on the client
that started it:
--lifecycle |
The daemon ends |
|---|---|
persistent (default for contour daemon) |
never on its own |
exit-with-last-session |
once its last hosted session is gone (tmux spells this exit-empty) |
exit-when-idle (what contour client starts on demand) |
10 seconds after it has no session and no client |
--exit-with-last-session is kept as an alias for --lifecycle=exit-with-last-session. Stating
both is an error only when they differ. A status probe (contour daemon status) is not a client and
never keeps an idle daemon alive.
contour daemon does not detach by itself
It runs in the foreground and logs to standard error, which is what you want under a service
manager or in a container. Background it with &, or let contour client start it for you.
Detaching versus ending a session
Because sessions outlive the window, "close" needs two answers — and Contour gives you both. Which one you get depends on what you close:
| What you close | What happens to the shell |
|---|---|
| The window (its close button, or quitting Contour) | Detach. It keeps running; the next contour client shows it again |
A pane — Close Pane in the right-click menu, or the ClosePane action |
Ends. That session is closed on the daemon and does not come back |
A tab — its ✕, or the CloseTab action |
Ends, together with every pane in it |
Typing exit in the shell |
Ends, as always — the shell decided |
This is the same split tmux draws between detaching a client and kill-pane, and it works the same
for a local session as for one hosted by a daemon: a pane you end is ended everywhere, including for
any other client attached to that daemon.
Neither ClosePane nor CloseTab ships with a default key binding — bind them yourself if you want
one:
Ending a whole daemon's worth of sessions
Closing panes one at a time is the fine-grained way. To end everything at once, stop the daemon — see Stopping a daemon.
Running several daemons side by side
Daemons are told apart by a label, exactly as tmux's -L does:
The label names a socket under your per-user runtime directory. Resolution order:
--socket PATH— used verbatim.- An explicitly given
--label NAME(the valuedefaultincluded) — the label's own socket, exactly as tmux's-Lbeats$TMUX. $CONTOUR_MUX— used verbatim (analogous to tmux's$TMUX).$XDG_RUNTIME_DIR/contour/<label>(the default label when none was given).<temp>/contour-<uid>/<label>when there is no runtime directory (tmux's/tmp/tmux-<uid>/<label>shape).
A daemon exports $CONTOUR_MUX into every shell it hosts, so a command typed in one of its panes
reaches that same daemon without a flag. That is why it ranks above the default label: a sandboxed
(Flatpak) shell cannot derive where its daemon lives, and a plain contour client there still finds
it. It ranks below an explicit --label, so contour daemon --label work status from inside a pane
asks the work daemon rather than the one hosting the pane.
The daemon binds three endpoints, all derived from that one path: the control socket itself,
<socket>-native for Contour's own clients, and <socket>-tmux for the tmux binary.
The socket directory is created 0700 and checked on every bind: it must be a real directory,
owned by you, and must not be world-accessible. Group access is permitted, matching tmux.
Keep socket paths short
A Unix socket path is limited to roughly 100 bytes by the operating system, not by Contour. A
deeply nested --socket path will fail to bind.
contour daemon options
| Option | What it does |
|---|---|
--socket PATH |
Path of the control socket file. Defaults to the configuration's daemon.socket, else $CONTOUR_MUX when no --label is given, else $XDG_RUNTIME_DIR/contour/LABEL. |
--label NAME |
Socket label distinguishing daemon instances. Default default. Given, it beats $CONTOUR_MUX. |
--config FILE |
Configuration file whose profile supplies the hosted sessions' terminal settings — including history.limit, which is how much scrollback a session keeps for clients that attach later. |
--profile NAME |
Which profile of that configuration to take them from. Defaults to the configuration's default profile. |
--lifecycle POLICY |
When the daemon ends on its own: persistent (the default), exit-with-last-session or exit-when-idle. See Quick start. |
--exit-with-last-session |
Alias for --lifecycle=exit-with-last-session. |
--size-policy POLICY |
Which attached client's size the shared grid takes when clients of different sizes are attached: latest, smallest or largest. Mirrors tmux's window-size. Defaults to the configuration's daemon.size_policy (latest unless set). |
--background |
Relaunches the command detached and returns once the daemon accepts connections. Without --log-file the daemon logs beside its socket (SOCKET.log), since a detached process has no console. |
--stdio |
Relays the native protocol between standard input/output and this user's daemon, starting it if needed: what contour client --host runs over ssh. Honours --label, --socket, --config, --profile, --log, --log-file and --size-policy (forwarded to a daemon it starts); cannot be combined with --listen-tcp, --background or --tmux-compat-socket. |
--in-place |
Runs from the installed binary instead of a per-version copy. See After upgrading Contour. |
--tmux-compat-socket LABEL |
Additionally binds tmux's own discovery path /tmp/tmux-<uid>/<LABEL>, so a plain tmux -L LABEL -C attach-session finds this daemon. |
--listen-tcp HOST:PORT |
Also serve the native protocol over TCP. Opt-in; always TLS-encrypted and token-authenticated. See Remote access. |
--token TOKEN |
Preshared token every TCP client must present. Required with --listen-tcp. Visible to other local users through the process list — prefer --token-file. |
--token-file FILE |
Reads the token from FILE instead, so the secret is protected by that file's permissions. Trailing newlines are ignored. |
--access-file FILE |
Further TCP tokens with narrower grants, one per line. Needs --listen-tcp. See Narrower tokens. |
--tls-cert FILE, --tls-key FILE |
PEM certificate and matching private key for the TCP listener. Omit both to have the daemon generate an ephemeral self-signed certificate. |
--log TAGS |
Enable logging for a comma-separated list of tags, or all. See Diagnostics. |
--log-file FILE |
Append log output to FILE instead of standard error; - means standard error explicitly. |
Hosting from a plain contour
The thin client is the default: a plain contour shows sessions that a daemon hosts, starting that
daemon when none runs. contour client always attaches; a plain contour decides by daemon.mode in
the configuration, or by --daemon-mode on its command line:
| Mode | Where the sessions run |
|---|---|
auto (the default) |
In the daemon, started if none runs; when none can be had, in the window's process, with a bar saying why — those shells end when the window closes |
in_process |
In the window's own process, as before daemon mode existed |
required |
In the daemon, or not at all: the launch ends with the reason before any window opens |
daemon:
mode: auto # auto, in_process or required
socket: "" # the daemon's socket path; empty selects the default
size_policy: latest # whose size wins when clients disagree: latest, smallest or largest
default_cell_pixel_size: { width: 10, height: 20 } # cell size images use until a client reports one
history_limit: 1000 # scrollback lines per session, -1 for unlimited; unset follows the profile
contour --daemon-mode in_process # this launch only: keep the sessions in this window
contour --daemon-mode required # this launch only: fail rather than fall back
What a launch opens
A launch brings back the workspaces of the daemon that nobody shows, one window each. A workspace that another window already shows is not mirrored: a second machine attaching to a daemon gets the workspaces the first one left, not a copy of what the first one is looking at. When every workspace is shown or hidden (see below), the launch opens one new workspace in one new window. A fresh daemon gets its first tab in the first window, as before.
A launch that names a program (-e, or a command after the options) runs it in the window's own process
under auto; under required it ends before any window, since a daemon session runs the host's profile.
When the connection is lost
A window attached to a daemon keeps its panes when the connection drops. They show "Reconnecting…"
while the window reconnects with backoff (250 ms doubling to 5 s) and picks its workspaces up again. A
connection gone half-open — a laptop that changed networks — is noticed by a heartbeat within about
15 s instead of whenever TCP gives up. Input typed while disconnected is dropped rather than replayed
into whatever shell answers next. A launch that falls back to in_process has no connection to lose.
Switching a window
A window shows one workspace of one daemon, over a connection of its own: each window has exactly one connection, so a window on a remote host does not tie up, or depend on, the other windows. You can point a window somewhere else without closing it, and its panes are swapped in place:
- To another workspace of the same daemon, with
SwitchToWorkspaceorCycleWorkspace. - To another daemon: a local one by its label (
contour daemon --label), or a saved host, withSwitchToDaemonorCycleDaemon. The window lands on that daemon's only workspace, on a new one when it has none, and asks which when it has several. - From the switcher popup (
ShowSwitcher), which lists every local daemon and saved host with their workspaces; New daemon… there (orNewDaemon) starts a local daemon under a label you give and opens a window on it. - From a shell, with
contour switch, which also works from a pane of the window being switched. The window asks first (see below).
The Open… variants (OpenWorkspace, OpenDaemon) open a new window on the destination instead. If a
window already shows the destination workspace, that window is focused and nothing else changes. The
actions and their palette rows are listed in Key Mapping;
none is bound to a key by default.
A switch that cannot finish leaves the window as it was and says why in a notice: the destination is not reachable, it has no such workspace, or it did not show the workspace within 10 seconds.
A switch asked for through the control API (contour switch, or any program calling workspace.switch) is
a program pointing your window somewhere, so the window asks before it switches: a prompt in the window
names the destination in full, the daemon by label or host and the workspace, and offers Deny or Switch.
Ticking Don't ask again keeps the answer, Deny or Switch, for every later switch between the same two
daemons (from the daemon the window is on to the destination's), whatever the workspace, until Contour
exits; it is never written down. A remembered Deny answers refused without asking. A destination named by
URL (ssh://…) asks every time and offers nothing to remember, since allowing it runs ssh with your
credentials. The switches you make yourself, by key, palette or the switcher popup, never ask. The GUI
cannot tell a script outside any pane from you typing contour switch, so that asks too.
Note
Windows of the in-process mode have no daemon behind them and cannot switch.
Shown, hidden and left workspaces
The daemon keeps one of three states per workspace:
| State | Meaning | Does a launch bring it back? |
|---|---|---|
shown |
A window shows it right now. | No: that window already has it. |
left |
No window shows it: its window was closed, or its machine went away. | Yes. |
hidden |
A window was switched away from it, and none shows it since. | No. |
Closing a window, or losing its connection, leaves the workspace left, so reopening Contour finds your
sessions where you left them. Switching a window away from a workspace hides it instead: you moved on
on purpose, so the next launch does not drag it back. A hidden workspace keeps running and keeps its state,
also across a daemon restart and a live upgrade; show it again by switching to it
(contour ls --json reports each workspace's state as visibility, see the
control API).
contour client options
| Option | What it does |
|---|---|
--socket PATH, --label NAME |
Which daemon to attach to; same resolution as above, daemon.socket of the client's configuration included. |
--workspace WORKSPACE |
Show only that daemon workspace instead of what a launch brings back, named as every control verb names one: its id or an id suffix (as contour ls prints it), its label, or its 1-based position. Fails, saying why, when it names no workspace or several. It also works with --host. |
--size-policy POLICY |
The policy a daemon this client auto-starts runs with; defaults to daemon.size_policy. |
--tmux |
Attach to a real tmux server instead of a Contour daemon. See tmux interoperability. |
--tmux-socket PATH |
The tmux server socket (tmux's -S) for --tmux. |
--profile NAME |
Config profile the window renders remote sessions with. |
--config FILE |
Configuration file to load. |
--host NAME\|URL |
Attach to a remote host: a name saved under hosts:, or an ad-hoc ssh://[USER@]HOST[:PORT] (a tls:// host needs its token, so it must be saved). The remote daemon is started there when none runs; the window title and the tab strip name the host. A host's profile: is used unless --profile names one. Cannot be combined with --socket, --connect-tcp, --token, --token-file, --tls-ca or --tmux. See Remote hosts. |
--connect-tcp HOST:PORT |
Connect to a remote daemon over TLS instead of a local socket. |
--token TOKEN, --token-file FILE |
The preshared token to present, inline or read from a file. |
--tls-ca FILE |
PEM trust anchor pinning the daemon's certificate. Omitted means encrypt-but-do-not-verify. |
--log TAGS, --log-file FILE |
As above — and passed on to a daemon this client auto-starts, which is otherwise the one instance nobody can configure. |
tmux interoperability
Contour speaks tmux's control-mode protocol in both directions, pinned to tmux 3.7b semantics.
Attach tmux tooling to a Contour daemon
<socket>-tmux speaks the binary protocol the stock tmux client uses, so the real tmux binary can
attach to a Contour daemon:
To let tmux find the daemon by label alone, have the daemon bind tmux's own discovery path too:
--tmux-compat-socket is opt-in for a reason
That path is where tmux looks for its server. With the Contour daemon down, a new-session
against it would silently start a real tmux server there instead.
Control mode (-C) is supported; -CC is not — Contour never renders a full terminal client for
tmux. Arbitrary startup commands are refused; the endpoint accepts attach- and new-session-shaped
startup only.
Show a real tmux server in a Contour window
The other direction: point Contour at a tmux server and mirror it.
contour client --tmux # the default tmux server
contour client --tmux --tmux-socket /tmp/x # a specific one
tmux windows become Contour tabs, tmux panes become splits, and input travels back as
send-keys. Content mirrors live.
tmux replay carries text, not images
Contour replays each pane's full history through capture-pane, so scrollback survives a
reattach on this path too. Images do not, and that is a tmux limitation rather than Contour's:
capture-pane serialises text and colour, never image data. Images drawn while you are
attached display normally; images drawn before you attached cannot be recovered. Contour's
own native protocol has no such gap — it transfers images by id.
Remote access over TCP
By default a daemon is reachable only through its Unix socket, where the filesystem permissions
are the access control. --listen-tcp additionally exposes the native protocol over the network:
# On the host running the sessions:
umask 077 && openssl rand -base64 32 > ~/.contour-token
contour daemon --listen-tcp 127.0.0.1:9090 --token-file ~/.contour-token
# On the machine you are sitting at:
contour client --connect-tcp server.example.com:9090 --token-file ~/.contour-token
Properties worth knowing before you open a port:
- TLS is not optional. The TCP endpoint is always encrypted; there is no plaintext mode.
- A token is required. TCP has no filesystem gate to fall back on, so
--listen-tcpwithout--token/--token-filerefuses to start rather than serving anonymously. - Prefer
--token-file. A token passed as--tokenis readable by every other user on the machine through the process list; a file carries permissions. - Without
--tls-cert/--tls-keythe daemon generates an ephemeral self-signed certificate, and without--tls-cathe client encrypts but does not verify who it is talking to. That is a trust-on-first-use posture: it stops passive eavesdropping, not an active machine-in-the-middle. For anything beyond a trusted network, pass a real certificate on the daemon and pin it with--tls-caon the client — the certificate must then also be issued for the host you connect to, not merely signed by that CA. - Bind deliberately.
127.0.0.1reaches only the local machine;0.0.0.0reaches everyone who can route to you. Prefer forwarding the loopback port over SSH to exposing it directly. - Control requests need a verified daemon. A client of the control API refuses to send a single
request over TLS it has not verified: without
--tls-cait stops before connecting. With the control API a token types into and reads every pane, so it must not be handed to whoever answers.
Narrower tokens for scripts
--token grants everything. A script that only needs to read a pane can be given a token of its
own that grants less, through --access-file FILE, one entry per line:
A line is a grant list — all, or a comma list of read (look at the workspaces), screen (read
what is on screen and in command blocks), input (type into a pane) and write (change the
workspaces, stop the daemon) — one space, then the token, which may contain spaces. Carriage returns
and trailing whitespace are dropped, as for --token-file; blank lines and lines starting with #
are ignored. An unknown grant, an empty token or a token that appears twice stops the daemon from
starting, naming the line. The file adds tokens beside --token/--token-file, never instead of
them: a hello may present either, and the --token keeps granting everything, so moving everyone to
the access file means removing --token as well. A line that repeats the --token stops the daemon
from starting too, since its narrower grants would silently read as everything.
Running a program is typing it: tab.create and pane.split with a command need input as well
as write, so a token granted read,screen,write can open shells and rearrange the workspaces but
cannot run a program of its choosing in it.
Events follow the same grants: block.finished carries the command line and needs screen, so a
subscription of a read token is handed every other kind but that one, and a subscribe naming only
kinds the token may not see is answered forbidden.
A call outside a token's grants is answered forbidden and the connection stays open. Viewing the
sessions needs every grant, so a contour client with a narrower token is refused exactly as with a
wrong one.
To attach over ssh instead, with no port to open, to save hosts by name, or to reach a home PC from the office over Tailscale, see Remote hosts.
Starting the daemon at login
Instead of letting contour client start a daemon when it is needed, you can have the operating
system start one for you when you log in:
contour daemon-service install # register it (and start it with your next login)
contour daemon-service status # registered? running? what does it run?
contour daemon-service start # start it now
contour daemon-service stop # stop it, leaving it registered
contour daemon-service restart
contour daemon-service uninstall # stop it and remove the registration
Each verb takes --label NAME (default default). install additionally takes --socket,
--config, --profile, --size-policy, --log and --log-file, which are recorded in the
registration. A socket path is made absolute at install time, because the registration outlives the
shell that wrote it. Unless you give --log-file the daemon logs to SOCKET.log.
| OS | What is registered | Runs as |
|---|---|---|
| Linux | the systemd user unit contour-daemon.service (contour-daemon-LABEL.service for other labels), written to ~/.config/systemd/user and enabled |
you, no elevation |
| macOS | the LaunchAgent org.contourterminal.contour-daemon in ~/Library/LaunchAgents |
you, no elevation |
| Windows | a Scheduled Task triggered by your logon | you, no elevation |
Packages for Linux also ship a contour-daemon.service unit file. It is not enabled by the
package; contour daemon-service install writes your own copy, which takes precedence.
daemon-service status and uninstall count the service as installed only once you registered it
with install; the packaged unit alone does not. A vendor unit enabled directly with systemctl --user enable or
--global enable is likewise not seen by daemon-service or the client's auto-start; use daemon-service install instead.
On Linux, a user's daemon stops when their last login session ends, unless lingering is on for that
user. When loginctl reports lingering as off, contour daemon-service install prints a one-line note
naming the command to run:
Contour never enables lingering itself. To install the daemon alone on a server, see Contour daemon on a server.
A service-managed daemon runs with --lifecycle=persistent: it never exits on its own, not even
with zero sessions and zero clients. contour client finds the registration and starts it before it
falls back to spawning a daemon on demand. A registration made by an older release under another
name is found as well.
--start boot and --start manual (a Windows service) are not implemented yet.
Stopping a daemon
Ask it to stop, from anywhere:
contour daemon stop # asks first when the daemon hosts sessions
contour daemon stop --yes # does not ask
contour daemon status # is one running? version, uptime, socket, sessions, clients
contour daemon logs # the last 100 lines of its log
contour daemon logs --follow --lines 20
All of them accept --label and --socket, written before or after the verb. Giving the same option
before and after the verb with different values is a usage error, so that stop never silently
targets another daemon than the one you named. When no daemon answers, status and stop exit with
status 4. stop asks for confirmation only when sessions exist; without a terminal it refuses unless
you pass --yes. logs shows the file the daemon reports as its log; if none is running it falls back
to the default SOCKET.log beside the socket, and says so.
A daemon started from a terminal also stops on SIGINT or SIGTERM — Ctrl+C
if it is in the foreground, or kill <pid>. On Windows, a console-control event does the same. A
daemon that predates contour daemon stop answers that it does not support it; stop it where it
runs, or end its process.
For a service-managed daemon, prefer contour daemon-service stop, which lets the service manager
know it was stopped on purpose rather than restarting it.
Shells do not survive the daemon
Persistence is against the client, not against the daemon process. Stopping the daemon ends
every shell it hosts. Shutdown is abrupt by design — attached clients see the connection close
with no goodbye, exactly as a real tmux kill-server looks to them. What comes back after a
reboot, a crash or an upgrade are the workspaces — see Surviving reboots.
Surviving reboots
A daemon keeps a snapshot of its workspaces, so that after a reboot, a crash or an upgrade the next daemon brings it back. The shells themselves cannot survive their daemon; what returns is:
- every workspace, tab and split, with its ratios, the active tab and pane, and a zoomed pane;
- tab titles and the tab colours you set;
- each pane's working directory and profile;
- the newest lines of each pane's scrollback, under a dim marker row such as
── restored · snapshot of 2026-10-24 09:15 ──, followed by a fresh shell.
The snapshot is written every interval seconds while the workspaces change, at once when you close
a pane or a tab, and once more as the daemon ends. It is restored when a daemon starts and finds
one: after a reboot, a crash, a SIGTERM, an idle exit, or the restart the drift chip offers after
an upgrade (see After upgrading Contour). It is not restored after a
plain contour daemon stop, which empties it, and panes and tabs you closed are not brought back.
contour daemon stop # ends the shells and discards the workspaces
contour daemon stop --keep-snapshot # ends the shells; the next daemon restores the workspaces
With no daemon running, contour daemon stop still offers to empty the saved workspaces, so a daemon
started later does not bring back what you meant to end. Since a crash may have left those workspaces as
all you have, it asks first, unless --yes is given, and without a terminal to ask on it clears
nothing and tells you to pass --yes. It exits with status 4 as before, whether it cleared the
workspaces or you kept them; with nothing saved it says so and creates no file. A
daemon from a release before this feature keeps no workspaces: --keep-snapshot then says so and
stops it plainly.
A program is never started again by itself. Only one you list in rerun_programs is run once more
when its pane was running it, and only when its command line holds nothing a shell would expand.
When the saved directory no longer exists, the pane opens in the shell's default directory and the
daemon logs it.
daemon:
restore:
scrollback: true # bring the scrollback back
scrollback_lines: 1000 # the most lines kept per pane, 0 to 100000
rerun_programs: [] # e.g. [vim, htop]; nothing is re-run unless listed
interval: 30 # seconds between writes of changed workspaces, at least 5
scrollback_lines bounds the snapshot only; how much scrollback a session holds while it runs stays
daemon.history_limit.
The snapshot is <state-dir>/sessions/<label>.snapshot (the state directory is listed under
Platform notes); a daemon started with --label work keeps work.snapshot. A
daemon given a socket of its own, by --socket or $CONTOUR_MUX, keeps <label>@<hash>.snapshot
instead, the hash naming its socket, so it never restores or overwrites the workspaces of the daemon
its label runs; contour daemon stop --socket finds the same file. The
file is your user's alone: mode 0600 in a 0700 directory on Linux, macOS and BSD, and a protected
ACL granting only your account on Windows. It is replaced atomically and durably: each write is
flushed to the disk before it replaces the previous snapshot, and the replacement is flushed in turn,
so a daemon crash, a power loss or a kernel crash in the middle of a write leaves either the previous
snapshot or the new one, whole.
The snapshot may hold secrets
Restored scrollback is whatever was on screen: a password echoed by mistake, a token printed by a
tool. It is written to disk in the clear. Set scrollback: false to keep only the layout and the
directories, or scrollback_lines: 0 for the same.
A snapshot that cannot be read is renamed <label>.snapshot.corrupt and the daemon starts with its
startup layout, logging why; one written by a newer release is left in place. A restore that brought
nothing back is tried once more at the next start, then set aside as <label>.snapshot.failed.
Diagnostics
The daemon logs through Contour's category system; contour list-debug-tags prints every tag.
The vthost.* categories are the daemon's:
| Category | Default | What it records |
|---|---|---|
vthost.daemon |
on | Endpoints bound, listeners started, signals, shutdown |
vthost.conn |
off | Per-connection accept, handshake, disconnect |
vthost.session |
off | Session spawn, resize, exit |
vthost.tmux |
off | Control-mode and tmux-binary attach or rejection |
vthost.client |
off | The client side of the native protocol |
vthost.trace.proto |
off | One line per protocol message, both directions |
vthost.trace.tmux |
off | Every control-mode line, both directions |
contour daemon --log 'vthost.conn,vthost.session'
contour daemon --log 'vthost.*' --log-file /var/log/contour-daemon.log
Three behaviours to know:
*is a plain prefix match, sovthost.*sweeps in the verbosevthost.trace.*tier as well. Namevthost.trace.*to select the trace tier alone.- A filter is a selection: it disables the categories it does not name. Real failures are exempt — asking for extra detail never takes error reporting away.
--log-fileappends and never writes colour escapes, so restarting a daemon against the same file does not erase why the previous run died.
Traces report sizes, never payloads: a keystroke message shows a byte count, a screen update a line count, and a token only as present or absent. A trace of a busy session must not become a transcript of what you typed.
Platform notes
Contour keeps what outlives a daemon in a per-user state directory, separate from the runtime directory the sockets live in:
| OS | State directory |
|---|---|
| Linux, macOS, BSD | $XDG_STATE_HOME/contour when set and absolute, otherwise ~/.local/state/contour |
| Windows | %LOCALAPPDATA%\contour |
Without a usable home directory the fallback is <temp>/contour-state-<user>. That directory is
shared ground, so a daemon never runs from a copy staged there: it runs in place instead.
Daemon mode works on Linux, macOS, BSD and Windows 10 1803 or newer, over Unix domain sockets
everywhere. On Windows, sessions are backed by ConPTY, the socket directory is governed by NTFS
ACLs rather than POSIX permission bits, and the tmux-binary (<socket>-tmux) endpoint is absent —
it needs file-descriptor passing, which Windows sockets do not have.
After upgrading Contour
An upgrade never touches the running daemon. The daemon keeps serving, and the client you start next may be newer than the daemon it attaches to.
Where an upgrade would disturb a running binary, the daemon does not run from the installed binary
at all. On Windows (the installer replaces contour.exe, and Windows Installer moves the images a
running copy holds aside rather than failing), on macOS (swapping the .app breaks the lazy loading of its frameworks) and for an
AppImage (its mount vanishes when the AppImage process exits), a daemon runs from a copy of its
own release under <state-dir>/bin/<version>-<id>/. It is made the first time that release starts a
daemon, and removed again once no daemon runs from it.
What the copy costs
A release contour binary is about 474 MB. On Windows the copy is the whole install folder, on
macOS the whole .app, and one such copy exists per release that has a daemon running or about to
start, until it is unused. Old copies are pruned automatically, never while a daemon runs from them.
A packaged Linux install (/usr/..., Flatpak /app/...) runs in place, which a package upgrade
cannot disturb. contour daemon --in-place opts out of the copy; a package upgrade may then replace
the binary under the running daemon. A hand-started foreground contour daemon on Windows also runs
from the installed binary.
Installing a new Contour on Windows
A daemon started by hand runs from the folder an installer is about to replace. Windows Installer moves the
files a running program holds aside, and that daemon would run on from its old images until it next
restarts. So the installer asks the Contour it replaces to move your hand-started daemons first: each one is handed over to a copy of itself under %LOCALAPPDATA%\contour\bin\, keeping its
sessions and their programs, exactly as a live upgrade does. The installer then replaces
the files. The same step is available as contour daemon relocate, which says for each daemon where it went
(moved the daemon at <socket> onto <executable>) and which it left (left the daemon at <socket>: <reason>).
A daemon that Contour starts for you already runs from such a copy, and needs none of this. What is not moved:
- A daemon started with
--in-place, which asked to stay where it is. A silent install replaces the files under it all the same (Windows Installer moves the images it holds aside), and it keeps running from its old images until it next restarts. What an interactive install does with it has not been verified: Windows may list it in its files-in-use dialog, and what "close" does to its sessions is not known. To be safe, stop such a daemon (contour daemon stop) before installing. - A daemon of an installation from before this change (0.8 and earlier, and earlier 0.9 development builds), which cannot be asked, and a daemon of another installation.
- A daemon whose socket is not in the default socket directory (
%TEMP%\contour-<user>\), such as one started with a--socketelsewhere.relocatelooks only in that directory. - Another user's daemon. The installer runs as the user who started it, so a UAC elevation as a different administrator account looks for that administrator's daemons, not yours; yours is left as it is.
- Any daemon, when a deployment tool installs as the SYSTEM account: SYSTEM has no daemon, so nothing is handed over, the install still succeeds, and your daemon keeps running from the old images until its next restart. Install as yourself to have it moved.
Not moving a daemon never fails the install.
After a move, the sessions' console host (OpenConsole.exe) keeps running from its old image, which Windows
Installer moves aside and deletes at the next restart; the new files are in bin\. The console host of a
session cannot be replaced while the session runs. Keeping it elsewhere is not available yet.
The installer also closes the running Contour window and opens it again with the arguments it had. That is Windows' Restart Manager, which closes and reopens a running Contour GUI. It does not end daemons: in a silent install it left a daemon, and its console host, running.
The two negotiate a protocol version when they connect: each states the range of versions it speaks, and they agree on the newest one both know. Every release speaks the protocols of at least the previous two releases, so a client from a newer release attaches to an older daemon — and an older client to a newer daemon — as long as the two are within that window. Features the older side does not know are simply not used on that connection.
When the daemon's release differs from the client's, the window shows a quiet chip, for instance
daemon 0.8.1 is older than this client. For a local daemon that supports it, clicking it offers
Upgrade daemon (keeps your programs running); see Live upgrade below. Otherwise
it offers Restart daemon on the new version. The confirmation says what that costs: it ends
every shell the daemon hosts, and a fresh one opens on the new daemon. A daemon that keeps its workspaces brings your workspaces, tabs,
directories and scrollback back, and the confirmation says so (see
Surviving reboots). After the restart a new Contour window attaches to the new
daemon, and this one closes. The chip is shown
only for the local daemon: a remote one (--connect-tcp) is not yours to restart.
The restart stops the daemon for an upgrade, then starts one the same way a client would: through the registered service when there is one, on demand otherwise.
Live upgrade
A live upgrade replaces the daemon with the new release without ending anything it hosts. On
Linux, macOS and the BSDs the daemon re-executes the new release's binary in its own process, so every
shell and program keeps its process, its process ID, its parent and its terminal. Windows cannot replace
a running process's binary: there the daemon starts the new release beside itself, hands it every
session, and ends once the new release has taken over. The daemon's process ID changes; every shell's and
program's does not, nor does its console. Each session's screens, scrollback, images,
cursor and modes, and an escape sequence cut in half by the switch (up to a bound, below), come across
as they were; so do your workspaces, tabs and splits. The windows show Upgrading daemon… for a moment and reconnect by
themselves.
Start it from the chip, or from a shell or script:
It exits 0 once the new release serves, 4 when no daemon runs, and 5 with the reason on standard error
when the daemon declines or the switch fails; the daemon then serves on as before. When the daemon says
nothing conclusive within 15 seconds, the outcome is unknown and the exit status is 1; check
contour daemon status, whose upgrades line counts the live upgrades the daemon has been through and
whose last upgrade line names the last one that failed.
What the daemon declines on:
| The daemon declines when | Because |
|---|---|
| it runs in a Flatpak sandbox | the sandbox cannot reach the new release's binary; update the Flatpak and use the restart |
| the new release cannot read the state it would hand over, or load its configuration | the check runs the new binary first; the restart is the way across a gap wider than two releases |
| a session runs on an SSH or channel connection | nothing could adopt it after the switch |
| a program is in the middle of an escape sequence longer than the handover carries, or its sessions' state is too large | the handover has a fixed bound |
| a session is still starting, or the new release's image is not in the daemon's store | try again in a moment; the next upgrade stages the image anew |
| another upgrade is under way, or the clients and sessions do not settle in time | try again in a moment |
on Windows, a session runs on Windows' own console host rather than the conpty.dll Contour ships |
only Contour's can be handed to the new daemon; it is missing only where conpty.dll is not beside contour.exe |
Most of these are checked before anything is stopped. A few show only after the daemon has told its
clients it is upgrading and disconnected them: the clients and sessions not settling in time, a session
that keeps starting, a program caught in an overlong escape sequence, state too large to hand over, and
the new binary failing to start. The daemon then resumes; the windows reconnect to it a moment later, and
the chip and contour daemon upgrade (exit 5) name the reason. On Windows the new release that does not
say it is ready within 8 seconds is ended, and the old daemon resumes the same way.
A refusal never costs a program, and neither does a failure after it: if the new binary cannot start, the old daemon resumes and says so.
A few things to know:
- The chip offers the upgrade only for a daemon on this machine that announced it supports it and is not
newer than the client (a newer one is offered the restart, as an upgrade would be a downgrade). A remote
daemon (
--connect-tcp,--host) is not yours to upgrade, and the chip offers nothing for it. - It is available on Linux, macOS, the BSDs and Windows.
- Flatpak: a running sandbox keeps the old
/appmounted, so the daemon cannot reach the new release. Update the Flatpak, then restart the daemon from the chip (this ends its shells). - The first release that has it can be upgraded from, not to. The readiness check needs code in the old daemon, so a daemon from before live upgrade is restarted once, and every daemon after that is upgraded live.
- Extensions that a status line runs in the daemon are started again by the new daemon; the programs in your panes are never restarted.
- The daemon keeps its workspace snapshot current, as always; it is the safety net if everything else fails.
Outside the window, or against a daemon from Contour 0.7.0 or another release before version negotiation, the client is refused, and it tells you both sides' versions:
A wrong token is reported as such instead (wrong token or insufficient rights): the refusal names
the daemon's versions either way, so a probe learns nothing more from it. To move past a version
refusal, stop the daemon and let it start again:
Remember that this ends the sessions it was hosting. An auto-started daemon rarely has the problem, since it exits 10 seconds after its last session and client are gone.
Current limitations
contour clientopens a window. There is no terminal-only client at present; if you need one, attach withtmux -Cthrough the compatibility endpoint described above.- You get the scrollback the daemon still holds, and no more. Attaching replays a session's
history, but the daemon keeps only
history.limitlines of it per session — 1000 unless the profile it was started with says otherwise. Lines that scrolled past that are gone, and there is no way to ask for them back. - A session sitting on the alternate screen has no scrollback to show. Attach while
vim,lessorhtopis running and you get that program's screen, which never has history of its own; the shell's scrollback appears when the program exits. - The daemon keeps one cell size. Images (Sixel, kitty, Contour's own) are laid out on the cell
size in pixels the attached client last reported, remembered after it detaches, and on
daemon.default_cell_pixel_sizeuntil one reports. With a per-pane font zoom, or windows on monitors of different DPI, every pane other than the last-reporting one lays its images out for that pane's cell, so they come out larger or smaller than its own cells would make them. - Shells end with the daemon (see Stopping a daemon); the workspaces come back without them (see Surviving reboots).
- ssh attach needs key or agent authentication. Contour never answers an ssh prompt — a
password, a key passphrase, a second factor or an unknown host key — so run
ssh USER@HOSTonce in a terminal first (see Remote hosts). - Images are not recoverable through tmux-mode replay (see the note under tmux interoperability).
For the architecture behind all of this — the two protocols, the delta transport, the stable row identity in the grid — see the internals page.