kit

Remote Sessions

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.

Requirements

Commands

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

Detachable by default

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.

Sessions on this machine

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.

Sessions and daemon restarts

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:

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:

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.

Versions and compatibility

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.

Session keys

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.

How pairing works

  1. kit daemon pair generates a fresh one-time code and opens a bootstrap endpoint for 10 minutes (or until one client pairs).
  2. kit remote --pair <code> proves knowledge of the code and presents the client’s signing public key.
  3. The host terminal shows the request (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.
  4. On accept, the client’s public key joins the host’s allowlist (~/.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.

How reconnection works

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.

Clipboard images

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.

Terminal and colors

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:

Security notes

systemd

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).

Troubleshooting

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