Skip to content

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

contour client

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:

input_mapping:
    - { mods: [Ctrl, Shift], key: 'W', action: ClosePane }

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:

contour daemon --label work
contour client --label work

The label names a socket under your per-user runtime directory. Resolution order:

  1. --socket PATH — used verbatim.
  2. An explicitly given --label NAME (the value default included) — the label's own socket, exactly as tmux's -L beats $TMUX.
  3. $CONTOUR_MUX — used verbatim (analogous to tmux's $TMUX).
  4. $XDG_RUNTIME_DIR/contour/<label> (the default label when none was given).
  5. <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 SwitchToWorkspace or CycleWorkspace.
  • To another daemon: a local one by its label (contour daemon --label), or a saved host, with SwitchToDaemon or CycleDaemon. 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 (or NewDaemon) 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:

tmux -S "$XDG_RUNTIME_DIR/contour/default-tmux" -C attach-session

To let tmux find the daemon by label alone, have the daemon bind tmux's own discovery path too:

contour daemon --tmux-compat-socket mine
tmux -L mine -C attach-session

--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-tcp without --token/--token-file refuses to start rather than serving anonymously.
  • Prefer --token-file. A token passed as --token is readable by every other user on the machine through the process list; a file carries permissions.
  • Without --tls-cert/--tls-key the daemon generates an ephemeral self-signed certificate, and without --tls-ca the 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-ca on 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.1 reaches only the local machine; 0.0.0.0 reaches 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-ca it 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:

# grants   token (to the end of the line)
read,screen ci-reader s3cret
all admin-token

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:

loginctl enable-linger

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, so vthost.* sweeps in the verbose vthost.trace.* tier as well. Name vthost.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-file appends 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 --socket elsewhere. relocate looks 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:

contour daemon upgrade [--label NAME | --socket PATH]

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 /app mounted, 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:

the daemon speaks protocol 3, from a release before version negotiation; this client speaks 4-4

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:

contour daemon stop
contour client                        # starts a matching daemon

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 client opens a window. There is no terminal-only client at present; if you need one, attach with tmux -C through 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.limit lines 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, less or htop is 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_size until 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@HOST once 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.