Kit can run as a daemon that hosts detachable sessions: a session keeps running when you close the terminal, and you can reattach to it later, switch between several like tmux, or share one with someone else. The daemon can be on this machine or on another.
When a daemon is running on this machine, kit itself is detachable —
there is nothing extra to type:
cd ~/project
kit # same command, same directory, same flags
# Ctrl+] d # detach; the agent keeps working
kit attach # come back to it
When no daemon is running, kit runs in your terminal exactly as it
always has. See Detachable by default for the
exact rule, and skip to Sessions on this
machine for the local setup — no pairing and
no configuration are needed.
The rest of this page covers remote sessions. All work — the agent, tools, extensions, sessions — happens on the daemon host; your local terminal just renders it. The transport is iroh: a direct, end-to-end encrypted QUIC connection that holes through NATs and falls back to relays.
Access is pairing-based. A client pairs with the host once — with a one-time code and an explicit accept/reject on the host’s terminal — and from then on reconnects by name with its own signing key. For normal reconnects no code is needed, and the host can revoke any client at any time. (If the host’s daemon identity file is deleted, its endpoint id rotates and every client pairs again.)
# On the host: start the daemon
kit daemon
# On the host: open a pairing window (shows a one-time code)
kit daemon pair
# On the client: pair (the host terminal asks you to accept)
kit remote --pair A1B2C3D4
Save this host as [workstation]: homelab
# On the client: connect — no code needed, ever again
kit remote --host homelab
On connection the daemon reports its live sessions; when any exist, an in-client picker lets you attach to one (the session’s screen repaints exactly where it left off) or start a new one — a fresh child shows the working-directory picker (starting in the daemon user’s home directory). The session behaves exactly like a local one: extensions, widgets, tool rendering, and session persistence all run on the daemon host.
kit build. The iroh transport is part of
the single kit binary — no extra components.| Command | Side | Purpose |
|---|---|---|
kit daemon |
host | Host sessions for paired clients |
kit daemon pair |
host | Open a 10-minute pairing window; confirm requests on this terminal |
kit daemon pair --list |
host | List paired clients with fingerprints |
kit daemon pair --revoke <fp> |
host | Revoke a paired client |
kit daemon status |
host | Endpoint, paired clients, active sessions |
kit remote --pair <code> |
client | Pair with a host and save it under a name |
kit remote --host <name> |
client | Connect to a paired host |
kit remote --list |
client | List saved hosts |
kit remote --forget <name> |
client | Forget a saved host |
kit attach |
either | Pick a session on this machine or any paired host (starts a local daemon if needed) |
kit attach --host <name> |
client | Attach on a paired host, with session switching |
kit attach --all |
client | Pick across every paired host without starting a local daemon first |
kit ls |
either | List live sessions (--all includes paired hosts) |
kit --no-daemon |
either | Run in this terminal, even when a daemon could host the session |
A plain kit becomes a daemon-hosted session whenever the local daemon is
running. The session is the same command: it starts in the directory
you ran it from, with the arguments you gave it, and with the credentials
and PATH from your shell. Nothing about the agent, the extensions, or
session persistence changes — only where the process lives.
Start the daemon once and it stays:
kit daemon service install # systemd user service (recommended)
kit daemon # or run it in a terminal
Kit runs in your terminal, undetachable, whenever hosting it would change what the command means:
| Case | Why |
|---|---|
| No daemon is running | Nothing to host it; this is the default state |
--no-daemon, or KIT_NO_DAEMON=1 |
You asked for this terminal |
daemon-mode: never in the config |
Same, as a standing preference |
A one-shot prompt (kit "...", kit @file.go "...") |
The caller is waiting for output, not for a session to detach from |
--quiet or --json |
Consumed by a pipe; a relayed terminal is the wrong shape |
| stdin or stdout is not a terminal | A session is a relayed PTY; there is nothing to relay |
| Already inside a session | Including a kit the agent runs from its own shell tool |
Set the standing preference in any .kit.yml:
daemon-mode: auto # host when a daemon is running (the default)
daemon-mode: never # always run in this terminal
daemon-mode: always # start a daemon on demand, then host
always starts a daemon for you the first time you need one. If it cannot
be started, kit runs in your terminal and says so rather than failing.
Only the local socket carries your directory, arguments and environment. A paired remote client never does — a directory from another machine names nothing here, and accepting an argument list from a peer would make pairing equivalent to arbitrary execution. Remote sessions therefore still open the working-directory picker, as they always have.
A session gets the full environment of the terminal you ran kit in,
as a kit run in that terminal would. This includes API keys for tools,
and the variables of a direnv or nix develop shell. A variable that you
unset in the terminal is also unset in the session. The daemon’s own
environment is not used, except for the variables the daemon owns (its
runtime and cache directories, the terminal description, and the
per-session files). The daemon never takes these from a client.
Variables that describe only your shell (PWD, OLDPWD, SHLVL, _,
SSH_TTY) are not sent.
A large environment is sent in more than one frame. A daemon from an older release cannot join the frames. For that daemon, kit removes the largest variables until the environment fits, tells you which variables it removed, and uses the daemon’s values for them. Restart the daemon with the current kit to send the full environment.
kit attach gives you the same detachable sessions without any pairing:
it talks to a daemon on this machine over a Unix socket in
$XDG_RUNTIME_DIR/kit/, and starts one if none is running. The socket is
0600 inside a 0700 directory and every connection’s peer uid is
checked, so only your own processes can reach it.
Where plain kit starts a session here, kit attach asks which
session — so it is the command to reach for when you want to go back to
something, or start one somewhere else. A new session from kit attach
opens the working-directory picker, rooted in the directory you ran it
from.
The picker it opens is not limited to this machine: it lists the local
daemon’s sessions followed by those on every paired host, each group under
its host name. Plain kit attach is therefore the one command that shows
everything you can attach to, wherever it is running — you only need
--host to go straight to a particular machine.
That means kit attach queries your paired hosts before it can draw, so a
host that is asleep costs the picker up to eight seconds (the same
per-host timeout as Ctrl+] w, and hosts are queried in parallel, so it
is eight seconds total rather than eight per host). kit attach --new and
kit attach <id> skip the query, because neither one opens a picker.
When nothing is live anywhere — no local sessions and no reachable remote
ones — kit attach skips the picker entirely and starts a new session,
which opens the working-directory picker.
kit attach --all lists the same sessions, with one difference: it draws
the picker before connecting anywhere, so choosing a remote session does
not start a local daemon you were not going to use. Plain kit attach
dials the local daemon first — starting one if none is running — because
that is where it looks for sessions before asking anyone else.
A session survives a client disconnect and a restart of the daemon. Each session runs in a supervisor process of its own, which owns the terminal and the kit process inside it; the daemon holds only a socket to that supervisor. Stopping, restarting or upgrading the daemon costs the socket and nothing else.
This is forced by the kernel rather than chosen. A pseudo-terminal master cannot be re-opened for an existing slave, so whichever process holds the master is the only one that can ever reach the session. Keeping it in the daemon made the daemon’s lifetime the session’s lifetime; moving it into a process whose only job is to hold it removes the coupling.
What that means in practice:
systemctl --user restart kit leaves every session running. The new
daemon scans for supervisor sockets, dials each one, and adopts the
sessions with their ids, names, working directories and uptimes intact.The daemon went away. Session N is still running — reconnecting…, and
reattaches by itself once a daemon answers. The supervisor replays the
recent output, so the screen comes back rather than staying blank.SIGKILL, a panic, or the OOM killer
takes the daemon and leaves the supervisors alone.KillMode=process for this reason. The
default (control-group) and KillMode=mixed both end by killing
everything left in the cgroup, which would destroy every session on an
ordinary restart.A session ends when the work inside it ends: you exit the kit TUI, or you stop the session deliberately. Nothing else stops it.
Two cases still end with the daemon, and both are reported rather than silent:
reattach capability,
so clients do not promise the user something the daemon cannot deliver.On the next start the daemon sweeps only what it can prove is both its own and unreachable: a session recorded by a previous run whose terminal died with it. A supervisor it could not talk to — one speaking another protocol version, say — is left strictly alone, because the work inside it is real and an unreadable supervisor is no reason to destroy it.
A session is only ever signalled when the daemon can prove it owns it: the process is checked against a marker inherited from the daemon that spawned it. A process that cannot be identified is left alone, so one daemon can never disturb another’s sessions, and a recycled pid can never be mistaken for a session.
Session ids are never reused. An id is written into the session’s socket name, into its scratch files, and into whatever you wrote down to reattach with, so a new session is always numbered above every id that has ever been handed out.
A client and a daemon are two separate installations of the same program, and nothing guarantees they were released together: upgrading the package replaces the binary on disk while the daemon started from the old one keeps running.
Compatibility is therefore decided by a protocol version, not by the release version. It changes only when the wire format changes in a way that an older peer genuinely cannot follow, which is rare. Everything added since — new frames, new fields, new capabilities — is negotiated through a feature bitmap instead: a peer that lacks a bit simply does not get that behaviour, and the connection is unaffected.
So a daemon from an older release and a client from a newer one talk normally, as long as the protocol version matches. A daemon so old that it predates the capability exchange entirely is also fine: saying nothing is read as “protocol v1, no optional features”, which is exactly what such a daemon is.
kit daemon status reports both numbers:
kit daemon is running (pid 4242, up 3d2h)
Build: v0.9.1
(this kit is v0.10.0 — restart the daemon to match)
Protocol: 1
Active sessions: 3
surviving a restart: 3
The build line is information, never a reason to refuse a connection. It is there for the moment you wonder why a feature you just installed is missing from a daemon you started three weeks ago.
Inside an attached session, Ctrl+] is the multiplexer prefix:
| Chord | Action |
|---|---|
Ctrl+] d |
Detach; the session keeps running |
Ctrl+] s |
Switch to another session |
Ctrl+] c |
Start a new session, in the same directory |
Ctrl+] n / Ctrl+] p |
Next / previous session |
Ctrl+] w |
Switch across paired hosts |
Ctrl+] Ctrl+] |
Send a literal Ctrl+] to the session |
The prefix is deliberately not Ctrl+X: that one belongs to the
session itself (Ctrl+X s steers, Ctrl+X e opens $EDITOR, and so on),
and every chord the client took would stop working over a connection. With
a separate prefix the keymap is identical whether kit runs locally or
through a session. Ctrl+X d still detaches, for compatibility.
Ctrl+] d detaches: your terminal returns to the local shell and the
session keeps running on the host — type /quit inside the session to end
it for good. The parting message names the session (kit attach 3, or
kit attach --host mev 3), so reattaching is a copy away. Detached
sessions are listed on the next connect, so you can pick up exactly where
you left off.
Ctrl+] w lists every other daemon: the paired hosts when you are on
this machine, and the paired hosts plus this machine when you are on a
remote one. There is always a way back home. Hosts are queried in
parallel, so one sleeping laptop no longer holds up the list; hosts that
do not answer within eight seconds are dropped from it, and kit ls --all
names them.
Several clients can also attach to the same session (tmux-style shared view): every attached terminal mirrors the same screen, keystrokes from any client go into the shared session, and the PTY is resized to the smallest attached window. Pairing is the authorization — any paired client may list and attach to any session. Attach rights end where pairing ends: revoking a client cuts off all of its sessions.
kit daemon pair generates a fresh one-time code and opens a bootstrap
endpoint for 10 minutes (or until one client pairs).kit remote --pair <code> proves knowledge of the code and presents the
client’s signing public key.client fp=379d…8510) and asks
Accept? [y/N] — the default is reject. Requests arriving while no
terminal can confirm (e.g. the service runs headless) are always denied.~/.config/kit/daemon/authorized.json), and the client stores the
host’s endpoint id (~/.config/kit/remote/hosts.json). The code is
burned.The code itself never grants access: it only makes the pairing window reachable, and a human still has to approve. Pairing requests that fail the code check never reach the prompt.
kit remote --host <name> dials the stored endpoint id and signs the
handshake with the client’s private signing key; the host verifies the
signature against its allowlist. iroh’s QUIC handshake additionally
authenticates the daemon against the stored endpoint id, so a malicious or
poisoned endpoint cannot impersonate the host.
Ctrl-V in a remote session attaches an image from the client
machine’s clipboard: the client reads its own clipboard (via xclip /
wl-paste / osascript), streams the image over the tunnel, and the daemon
hands it to the session as a pending attachment — the same inline
thumbnail preview and [N image(s) attached] indicator a local paste
gets. Add your text and submit; Ctrl-U clears it.
Ctrl-V with no image on the clipboard is forwarded to the session
unchanged, so host-side Ctrl-V behavior (e.g. quoted-insert in an
embedded shell) is preserved.KIT_IMAGE_PROTOCOL works here too.Ctrl-V through the kitty keyboard
protocol) and in legacy terminals alike.A session renders into your terminal, but the daemon that spawns it
cannot see that terminal: between the two sits a pseudo-terminal, which
reports no color depth and answers no background-color query. So the
client describes its own terminal when it attaches, and the daemon starts
the session against that description — much as ssh forwards TERM.
Four things are forwarded:
| Value | Source on the client | Seen by the session as |
|---|---|---|
TERM |
the client’s environment | TERM |
COLORTERM |
the client’s environment | COLORTERM |
| Terminal background color | an OSC query the client runs before it hands over the terminal | KIT_REMOTE_BACKGROUND |
| Terminal multiplexer | TMUX / ZELLIJ / TERM on the client |
KIT_REMOTE_MULTIPLEXER |
The background color is what decides whether a theme renders its light or
its dark palette, so it is resolved on the client: the session’s own query
would have to cross the pseudo-terminal — and, for a remote session, the
network — before it could draw its first frame. A terminal that does not
answer is forwarded as unknown, which reads as dark and stops the
session asking a question already known to go unanswered.
This applies to local sessions too, not just remote ones: a daemon outlives the terminal that started it, and one started by systemd never had a terminal at all. Without the forward, a session inherits the daemon’s environment and a 24-bit theme degrades to 256 colors. That degradation is not an even dimming — every color snaps to the nearest cell of a coarse cube, so the near-black surfaces a dark theme is built from land on saturated corners and the session appears in colors the theme never defines.
A few details worth knowing:
TMUX and ZELLIJ name the pane of the process that reads them, so
they never cross the wire, and a session that cannot see the
multiplexer draws inline graphics the multiplexer throws away. The
daemon’s own multiplexer variables are dropped along with them — a
daemon started from inside tmux describes a pane on the wrong machine.TERM but no COLORTERM, the daemon’s own
COLORTERM is dropped rather than inherited — claiming a color depth
your terminal never advertised is the same misreport in the other
direction.TERM, the session falls back to
xterm-256color./theme, config files, and the persisted preference are all
read on the daemon host.~/.config/kit/remote/identity.key
(0600). The host stores only public keys — there are no shared secrets.~/.config/kit/daemon/identity.key changes its
endpoint id; every client must pair again.kit daemon pair --revoke <fp>
(prefix matching works; ambiguous prefixes are refused).kit daemon status.kit daemon service install # writes ~/.config/systemd/user/kit.service, enables + starts it
kit daemon status # endpoint, paired clients, active sessions
systemctl --user status kit # manage the service directly
kit daemon service remove # stop and uninstall
install captures provider credentials from your current shell
(*_API_KEY, *_TOKEN, PROVIDER_*, and similar) into
~/.config/kit/daemon.env, which the unit loads via EnvironmentFile. Edit
that file and run systemctl --user restart kit when keys change. The
service runs without a terminal, so pairing requests are denied while it is
the only thing running — pair interactively with kit daemon pair (the
allowlist is shared).
| Symptom | Cause and fix |
|---|---|
another instance is already running |
A daemon (or service) already holds the lock — see kit daemon status |
no daemon is live for this pairing code |
The pairing window expired or a client already paired; open a new one with kit daemon pair |
the pairing request was rejected on the host |
The request reached the host and was declined; ask the host user to rerun kit daemon pair |
the host no longer knows this machine |
The client was revoked — pair again |
could not reach the daemon |
Network or relay issue; check connectivity on both sides |
Session dies with API key not provided |
The daemon environment is missing provider keys — for the systemd service, edit ~/.config/kit/daemon.env and restart |
| Detached by accident | Just reconnect with kit remote --host <name>; the daemon keeps running |