Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

zuko docs

Private remote shells for machines you own—without inbound ports or a VPN.

zuko’s supported core is a Linux/macOS host and Rust CLI. Iroh handles dial-by-key reachability, relay fallback, NAT traversal, and encrypted transport; zuko handles a real PTY, one-time-code pairing, client authorization, and short reconnects.

Fast path

curl https://mise.run | sh
mise use --global github:adonm/zuko
zuko install
zuko share        # on host
zuko <code>       # on client

Use tmux, zellij, or screen for durable work. zuko deliberately does not store detached output or promise that PTYs survive a host restart.

Useful commands:

zuko ls
zuko rm <name>
zuko reset
zuko doctor
zuko upgrade --check
zuko app --doctor

The iOS/iPadOS client is Beta. The browser client and zuko app are Labs surfaces with narrower support; see the roadmap before depending on them.

Source: github.com/adonm/zuko.

Direction and roadmap

North star

zuko provides private remote shells for machines you own, without opening inbound ports or operating a VPN.

The primary user is a developer, self-hoster, or small operator connecting to a personal Linux or macOS machine. The complete core workflow should stay short:

  1. install a per-user host service;
  2. pair a device with a one-time code;
  3. reconnect by a memorable name;
  4. survive brief network changes;
  5. inspect or revoke access locally.

Iroh owns network reachability and encrypted transport. zuko owns the PTY, pairing, authorization, reconnect behavior, and clear operator feedback.

Product tiers

TierMeaningCurrent surfaces
CorePrimary maintained and release-gated workflowLinux/macOS host and Rust CLI
BetaIntended for regular use, but availability or compatibility is not yet stableiOS/iPadOS client
LabsOpt-in experiments used to learn; APIs and behavior may changeBrowser client and Linux zuko app

A surface moves up a tier only when it has a clear install path, recovery behavior, security boundary, automated coverage, and maintained documentation. Code existing in the repository is not by itself a support commitment.

Current priority: make the core boring

Work toward the next release should improve the host + CLI workflow before adding another client or transport:

  • make service state and connection failures easy to diagnose;
  • test pairing, authorization, revocation, reconnect, and shell-exit behavior;
  • make install, upgrade, reset, and recovery steps predictable on Linux/macOS;
  • document version/protocol compatibility and return actionable errors;
  • keep queues, retries, handshakes, and detached leases bounded;
  • keep secret storage and the pairing trust boundary reviewable.

Success means a user can recover from a stale pairing, lost client, relay change, service restart, or interrupted upgrade without understanding Iroh or reading source code.

Completed foundations

  • 0.8.5: added one read-only zuko doctor path for service, host key, ticket, local trust state, and bounded Iroh relay checks;
  • 0.8.5: added live-network coverage proving revoked clients receive a permanent authorization error instead of entering a reconnect loop;
  • 0.8.5: documented the protocol compatibility boundary and aligned the security model across native and browser clients.

Toward 1.0

The core is ready for a 1.0 stability promise when all of these are true:

  • the host and CLI have a documented compatibility policy;
  • Linux and macOS install, upgrade, reset, and uninstall paths are exercised in release checks;
  • end-to-end tests cover authorization failure and transient reconnects as well as initial pairing and PTY I/O;
  • protocol fixtures are shared with non-Rust clients;
  • security documentation matches the implementation and has had focused review;
  • releases state supported platforms, known limitations, and migration steps.

No calendar date is attached to 1.0. These outcomes are the gate.

Beta and Labs promotion

iOS/iPadOS beta

Promote to Core after there is a documented public install path, a sustainable deployment target, release compatibility checks, and parity for reconnect and authorization errors. Until then it remains a useful source-built/TestFlight beta rather than a generally available client.

Browser Labs client

Keep in Labs until it has reconnect/backoff, browser-level tests, and storage on a dedicated hardened origin. Browser Iroh remains relay-only. If those costs do not justify the use case, keep it as a pairing/protocol demonstration rather than expanding the core promise.

zuko app Labs feature

Keep the Kitty/cage path opt-in. Promote only if terminal compatibility, failure recovery, runtime dependencies, and interactive performance are reliable enough to support. Native video or a separate remote-desktop protocol is not on the active roadmap.

Explicitly deferred

These may be reconsidered when a demonstrated user need outweighs their ongoing cost, but they are not current goals:

  • Android and additional native clients;
  • durable PTY storage or output replay (use tmux, zellij, or screen);
  • full desktop streaming or a native video protocol;
  • centralized accounts, RBAC, audit pipelines, or enterprise fleet management;
  • zero-downtime daemon upgrades;
  • broad plugin or protocol-negotiation frameworks without a concrete client.

Decision filter

Prefer work that makes the core workflow safer, faster to understand, easier to recover, or easier to verify. A proposal that adds a platform, protocol, or long-running service should identify the core user problem, maintenance cost, security boundary, tests, and promotion tier before implementation.

When priorities conflict, use this order:

  1. prevent unauthorized shell access or secret loss;
  2. preserve shell correctness and recoverability;
  3. improve pairing, diagnostics, and trust management;
  4. improve resource use and maintainability;
  5. expand Beta or Labs capabilities.

Host & CLI

zuko is one binary: host daemon, CLI client, pairing helper, service installer, upgrader, and Linux zuko app launcher.

Commands

zuko host              # foreground host
zuko install           # install/start user service
zuko uninstall         # remove service; keep user state
zuko upgrade           # mise-managed binary upgrade
zuko doctor            # service, ticket, state, network diagnostics

zuko share             # authorise a client with a one-time code
zuko <code>            # claim, save, connect
zuko claim <code>      # flags: --as, --no-connect, --timeout

zuko <name>            # connect to saved host
zuko connect <name>
zuko                   # TTY picker / non-TTY list
zuko ls                # saved hosts + authorised clients
zuko rm <name>         # remove saved host and/or authorised client
zuko reset             # rotate host key; clear authorised clients

zuko app <command>     # Linux GUI app over Kitty graphics

State

PathRole
~/.config/zuko/keyhost identity
~/.config/zuko/current_ticketlive dial ticket, refreshed by host
~/.config/zuko/authorized_clientshost allow-list
~/.config/zuko/hostsclient-side saved hosts
~/.config/zuko/client_keyCLI client token seed

All secret state is user-local and written 0600 where Unix permissions apply.

Install service

curl https://mise.run | sh
mise use --global github:adonm/zuko
zuko install

Linux:

journalctl --user -u zuko-host -f
sudo loginctl enable-linger "$USER"   # servers that must run without login

macOS:

tail -f ~/.config/zuko/zuko-host.err.log

Install flags:

FlagDefault
--prefix~/.local
--key~/.config/zuko/key
--shell$SHELL
--no-startdisabled

Foreground host:

zuko host --shell /bin/bash --cwd "$HOME"

Pair and connect

# host
zuko share

# client
zuko <code>

claim saves the ticket under the host label unless --as <name> is set. On an interactive terminal, share also renders a QR containing only the one-time code for the iOS scanner. The long-lived ticket is never in the QR, and stdout remains the plain code so scripts can continue to pipe it. After pairing:

zuko ls
zuko <name>

The host admits only tokens in authorized_clients. Pairing writes that list.

Trust management

zuko ls
zuko rm ipad
zuko reset
zuko reset --yes

reset removes key, removes current_ticket, and writes an empty authorized_clients. Restart the host, then re-pair each client.

Session behavior

  • Host runs a real PTY with TERM=xterm-256color.
  • Shell exit ends the session and kills the PTY.
  • Network/client drop detaches the PTY for 5 minutes.
  • Detached output is discarded.
  • CLI reconnects while the process is alive; iOS redials while screen is active.
  • Use tmux, zellij, or screen for durable work.

Force-exit a stuck CLI: Ctrl-C three times within ~1s with no remote output.

Pairing internals

share derives a throwaway Iroh key from the code, serves <label>\n<ticket> over ALPN zuko/handoff/1, then receives the client’s AUTHORIZE frame. claim retries the handoff dial for --timeout seconds (default 60). share reads current_ticket; interactively it offers to start the service when that file is unavailable, while non-interactive use fails.

zuko share flags:

FlagDefaultNotes
--ticketcurrent_ticketadvanced override; argv may expose the ticket, so prefer the file
--labelhostnamedefault save name on client
--count1accepted claims before exit
--timeout300seconds; 0 waits forever

Upgrade

zuko upgrade --check
zuko upgrade
zuko upgrade --version 0.8.0
zuko upgrade --no-restart

Only works for mise-managed installs. Restarting the service kills in-memory PTYs.

Debug

RUST_LOG=iroh=info zuko home
RUST_LOG=iroh=debug zuko home

Host logs are stderr in foreground, systemd journal on Linux, and ~/.config/zuko/zuko-host.err.log on macOS.

Troubleshooting

Start with the read-only diagnostic report:

zuko doctor

It checks whether the platform user service is installed and active, validates the host key and fresh ticket without printing either, summarizes saved hosts and authorized clients, and performs a 10-second Iroh relay-registration probe. Warnings include the next command to run; a client-only installation may legitimately warn that no local host is installed. Pass --key <path> if the host service was installed with a non-default key path.

zuko share reports a missing or stale ticket

The host service must be running and refreshing current_ticket. Check its log, then restart it with the platform service manager or run zuko host in the foreground. Do not copy a raw ticket around as a workaround.

The host rejects authorization

The client’s token is no longer in authorized_clients, usually after zuko rm or zuko reset. Run zuko share on the host and claim the new code from that client. Repeated dialing cannot repair an authorization failure.

A reconnect opens a fresh shell

The detached lease lasts 5 minutes and exists only in the host process. Expired leases, host restarts, and upgrades create a fresh PTY. Use a terminal multiplexer for durable work.

A connected full-screen app looks stale

Resize the local terminal to trigger a repaint. On iOS, use the Refresh action. If the link is wedged, use the CLI force-exit sequence and reconnect.

Build/test

cargo build --release
cargo test
cargo test --release --test e2e -- --ignored --nocapture

The ignored e2e test uses a real PTY and the live Iroh network.

zuko app (Labs)

Linux-only GUI app streaming. Run inside an existing zuko <host> shell.

This is an opt-in experiment, not part of the supported remote-shell core or a promise to become a full remote desktop. Expect terminal-compatibility, performance, and runtime-dependency gaps. See the roadmap.

Implementation: host spawns headless cage/wlroots, captures frames, writes Kitty graphics to stdout, and injects keyboard/mouse input back into cage.

Quick use

zuko app --list
zuko app text-editor
zuko app firefox
zuko app --fps 5 -- firefox --new-window

Flags go before the child command. Use -- before child flags.

Diagnostics

Run in this order:

zuko app --test-pattern
zuko app --doctor
zuko app --dry-run firefox
zuko app --debug-child firefox

If --test-pattern fails, fix terminal Kitty graphics or the zuko PTY path before debugging cage/app launch.

Flags

FlagDefaultNotes
--listlist aliases
--dry-runprint launch command/env
--test-patterndraw Kitty test image; no cage
--doctorcheck cage/protocol/geometry
--debug-childlet child stdout/stderr through
--no-sandboxbrowser/container escape hatch
--no-cursorhide crosshair cursor overlay
--fps30max frame rate
--max-mbps80approximate graphics bandwidth cap; 0 disables
--graphics-codecautoauto, png, rgb
--scale1.0render below/above terminal pixel size
--softwareforce software GL/WebRender in child

Flatpak

Flatpak launches are Wayland-only cage children with --die-with-parent. zuko detects exported Flatpaks and simple Exec=flatpak run <app-id> ... desktop files.

Portal-heavy/full-desktop flows: run an RDP client inside zuko app and connect to GNOME/KDE RDP on the host.

Runtime deps

x86_64 Linux release tarballs bundle cage plus uncommon wlroots libs next to the zuko binary. Lookup order:

  1. <exe_dir>/cage/
  2. ~/.local/share/zuko/cage
  3. PATH
  4. $ZUKO_CAGE

Not bundled: libwayland, libxkbcommon, libdrm, libxcb, libinput, libudev, mesa libEGL/libGLESv2.

aarch64 Linux currently needs cage on PATH.

Wire protocol

Transport

  • Iroh QUIC endpoint ticket (endpointa…).
  • Session ALPN: zuko/2.
  • Handoff ALPN: zuko/handoff/1.
  • First bidi stream is data. Optional second bidi stream is control.

Frame format

[type: u8][len: u16 BE][payload: len bytes]

len excludes the 3-byte header. Max payload: 65535 bytes. Receivers must accumulate and parse greedily; frames can split/coalesce across QUIC reads.

Frame types

TypeNameDirectionPayload
0x00DATAbothterminal bytes
0x01RESIZEclient → hostcols:u16 rows:u16 pixel_width:u16 pixel_height:u16
0x04PINGbothnonce:u64
0x05PONGbothnonce:u64
0x06ATTACHclient → hosttoken:16 bytes + resize payload
0x07ATTACHEDhost → clienttoken:16 bytes
0x08AUTHORIZEclient → handoff hosttoken:16 bytes + UTF-8 label
0x09ERRORhost → clientcode:u8 + UTF-8 message

Unknown types are ignored.

ERROR is fatal. A client must show the message and stop reconnecting. Defined codes are 0x01 (authorization failure; pair again) and 0x02 (protocol violation).

Session handshake

  1. Client dials host ticket on zuko/2.
  2. Client opens data bidi stream.
  3. First frame must be ATTACH with a non-zero host-scoped token and current terminal size.
  4. Host checks authorized_clients for the token.
  5. Host creates or reattaches the PTY keyed by that token.
  6. Host sends ATTACHED(token).

The token identifies both an authorized client and that client’s in-memory PTY lease. A second connection with the same token takes over the same PTY.

RESIZE is valid only after ATTACH. Cell dimensions are clamped to at least 1x1. Pixel dimensions may be zero.

Pump

  • Keystrokes/stdin: DATA client → host.
  • PTY output: DATA host → client.
  • Size changes: RESIZE on control stream when available, otherwise data stream.
  • PING replies with PONG carrying the same nonce.

Shell EOF closes the stream and kills the PTY. Network/client drop detaches the PTY for 5 minutes; output while detached is discarded.

Compatibility

  • The ALPN is the incompatible-version boundary. Current peers support only zuko/2; there is no version negotiation or v1 fallback.
  • New optional frame types may be added to zuko/2; receivers ignore unknown types.
  • Existing frame meanings and required handshake order must not change within zuko/2.
  • A deliberate host rejection uses ERROR and must not enter a retry loop.
  • Malformed required frames or an unexpected stream close are fatal to that connection.

Ticket handoff

Purpose: let a client learn the host ticket and register its future ATTACH token without putting the ticket on argv/stdin/stdout.

Possession of the short code while zuko share is active grants enrollment. A share accepts one claim by default; --count can explicitly allow more. Treat the code as temporary sensitive data and do not leave an unlimited share (--timeout 0) unattended.

Host (zuko share):

  1. Generate memorable code.
  2. Derive throwaway Iroh secret: Argon2id(normalized_code, salt="zuko-share-handoff-v1") -> 32-byte seed.
  3. Bind endpoint on zuko/handoff/1.
  4. Open uni stream and write:
    <label>\n<ticket>
    
  5. Wait briefly for client AUTHORIZE uni stream.
  6. Save token + label to authorized_clients.

Client (zuko claim / zuko <code>):

  1. Derive same throwaway endpoint id from code.
  2. Dial zuko/handoff/1, retrying until timeout.
  3. Read label + ticket.
  4. Parse ticket host id.
  5. Derive stable host-scoped token from local client secret + host id.
  6. Open uni stream, send AUTHORIZE(token, client_label).
  7. Save ticket locally and optionally connect.

Token derivation

Rust CLI:

SHA256("zuko-session-token-v1" || client_key_bytes || host_id_bytes)[0..16]

The browser client uses the same derivation with its IndexedDB client key.

iOS:

SHA256("zuko-ios-session-token-v1" || keychain_seed || host_id_string)[0..16]

Tokens must be non-zero.

Security notes

  • A shell connection requires host dial information plus a token in the host’s authorized-client list.
  • Host key stays on host at ~/.config/zuko/key.
  • Host admits only authorised client tokens.
  • Iroh provides transport encryption; relays see encrypted traffic.
  • Rotate host trust with zuko reset, restart, then re-pair clients.

Reference implementations: src/wire.rs, src/client.rs, src/host.rs, src/handoff.rs, ios/ZukoWire/, ios/Zuko/Zuko/Net/, and web/wasm/src/lib.rs.

Clients

Client tiers follow the roadmap:

ClientStatusSource
Rust CLICoreLinux/macOS release binary; src/client.rs
iOS/iPadOSBetaiOS/iPadOS 26.5; ios/Zuko/
WebLabsOpen client; web/

Core is release-gated and supported. Beta is intended for use but still has availability or compatibility constraints. Labs is opt-in and may have known reliability or security-boundary gaps.

The iOS/iPadOS app is built in CI and has a signed/TestFlight release pipeline, but this repository does not currently document a public TestFlight or App Store install path. Build instructions are in ios/Zuko/README.md.

The web client is published with the docs. It uses browser Iroh over relays, lacks automatic reconnect, and stores connection state in IndexedDB. See Targets for its promotion criteria, security boundary, and known gaps.

Implementing a client

Read protocol.md. Checklist:

  1. Claim ticket via zuko/handoff/1:
    • derive handoff key from code;
    • dial throwaway endpoint;
    • read <label>\n<ticket>;
    • derive host-scoped token;
    • send AUTHORIZE before closing handoff connection.
  2. Persist a client secret. Derive non-zero token from (client secret, host id).
  3. Dial host ticket with ALPN zuko/2.
  4. open_bi; first frame must be ATTACH(token, cols, rows, pixels).
  5. Pump length-prefixed frames:
    • stdin/terminal bytes → DATA;
    • remote DATA → terminal emulator;
    • size changes → RESIZE;
    • optional control stream for RESIZE/PING/PONG.
  6. Store ATTACHED token. Reuse it for short reconnects.
  7. Surface ERROR as fatal; do not retry an authorization or protocol failure.

Operational details:

  • First frame is mandatory; Iroh exposes streams to the peer after initiator data.
  • Clamp terminal cells to at least 1x1.
  • Serialise writes; frame interleaving corrupts the stream.
  • Bound outbound queues. Dropping impatient input is better than unbounded memory.
  • Forward terminal bytes verbatim. Local Ctrl-C handling should be an explicit escape hatch only.
  • Clean EOF means shell exit. Redial transient link errors.
  • Apply bounded backoff and stop reconnecting when the user leaves the session.

Mobile clients should call the Rust FFI derive_handoff_key(code) instead of reimplementing Argon2id. See src/ffi.rs and ios/Zuko/.

Reference code:

  • Rust framing/session: src/wire.rs, src/client.rs, src/handoff.rs
  • Swift framing/session: ios/ZukoWire/, ios/Zuko/Zuko/Net/
  • Browser framing/session: web/wasm/src/lib.rs, web/src/

Design principles

The product direction lives in the roadmap. These principles turn it into engineering constraints.

Product boundary

zuko is a terminal-first, per-user remote shell for machines the user owns. The Core product is the Linux/macOS host and Rust CLI. Mobile, browser, and GUI app streaming must not make that path harder to install, secure, debug, or maintain.

zuko does not own network reachability, durable terminal sessions, or fleet identity. Iroh provides reachability and encrypted transport; terminal multiplexers provide durable work.

Priorities

When a design trades one property for another, prefer this order:

  1. explicit authorization and safe local secret storage;
  2. correct PTY behavior and bounded failure modes;
  3. actionable operator feedback and recovery;
  4. protocol simplicity and compatibility;
  5. optional client or streaming features.

Constraints

  • The base experience is a real PTY over Iroh.
  • A stock terminal and one binary must remain a useful client.
  • Pairing is explicit, short-lived, and separate from ordinary connections.
  • Host state remains inspectable under ~/.config/zuko.
  • Handshakes, queues, retries, and detached leases are bounded.
  • A brief disconnect may reattach; detached output is not replayed.
  • Long-running work belongs in tmux, zellij, or screen.
  • New background services and trust surfaces require a concrete Core use case.

Protocol shape

Session ALPN: zuko/2.

  • data stream: ATTACH, DATA, ATTACHED, ERROR;
  • control frames: RESIZE, PING, PONG on an optional control stream, with data-stream fallback;
  • handoff ALPN: zuko/handoff/1;
  • authorization: AUTHORIZE during handoff, enforced on ATTACH.

Prefer additive frame types over negotiation layers until a concrete client needs more. Unknown frame types are ignored; a new incompatible handshake gets a new ALPN.

Labs: zuko app

The current GUI path runs cage/wlroots on the host, sends Kitty graphics over the existing PTY, and maps terminal input back into cage. Reusing the shell path avoids a second listener, client, and pairing surface.

This remains an experiment, not the start of a remote-desktop stack. A native video protocol should be considered only after a demonstrated use case and an explicit maintenance/security plan.

Labs targets

Labs code is useful for validating demand and protocol portability, but is not part of the Core support promise. Promotion criteria are tracked in the roadmap.

Browser client

Goal: test whether a zero-install browser can be a useful zuko client without weakening the Core host/CLI design. The static app at /web/ can claim and connect to a host today.

Open it from the published docs: zuko web.

Chosen stack:

  • Transport: Iroh’s browser/WASM build. Browser peers are relay-only because the browser sandbox cannot do UDP hole punching, but Iroh still keeps payloads end-to-end encrypted.
  • Terminal: wterm with @wterm/ghostty, which uses a libghostty VT core compiled to WASM. This is the best Ghostty-derived browser terminal available today; it is not the full native Ghostty app.
  • UI: semantic HTML plus bundled Bootstrap 5 CSS. No CDN scripts/styles are loaded at runtime; Vite emits static assets under /web/.
  • Build: Deno runs TypeScript/Vite tasks and consumes npm packages through package.json + deno.lock.
  • Storage: IndexedDB under the Pages origin. It stores the browser client key and claimed host connection information locally.

Security boundaries:

  • Shell access requires the stored host connection information and the browser’s authorized client key. Any script running on the same origin can read both from IndexedDB and impersonate that browser client.
  • Keep the web app dependency-free at runtime except bundled assets, use a strict CSP, and do not load third-party scripts anywhere on the shared origin.
  • Browser Iroh traffic is relay-only. Relays can see metadata and volume, not decrypted zuko frames.

Implementation status:

  • Static app: web/, built by mise run build-web into target/book/web/.
  • CI publish: .github/workflows/docs.yml builds mdBook first, then the web app, and uploads one Pages artifact.
  • Iroh bridge: web/wasm/ exposes claim/connect/session streaming to TypeScript.
  • Terminal UI: @wterm/ghostty renders the remote PTY stream in the browser.
  • Rendering hygiene: remote output is batched per animation frame, resize frames are debounced, and the browser sends measured grid pixels instead of the outer element size.

Known gaps:

  • No reconnect/backoff loop for an already-attached terminal session yet.
  • zuko app Kitty graphics should be treated as experimental until wterm’s renderer is verified against zuko’s graphics stream.
  • Pages security headers are limited compared with a dedicated app origin; move to a hardened subdomain if this becomes a daily-use client.

Promotion requires reconnect/backoff, browser-level tests, and isolation on a dedicated hardened origin. Until then, use the Core CLI for routine access.

Releasing

Tags v* trigger binary releases for:

  • Linux x86_64/aarch64
  • macOS x86_64/aarch64

The release tarballs are consumed by:

mise use --global github:adonm/zuko

Cut a release

mise run test
mise run lint          # if Swift changed
mise run build-ios     # if iOS changed
mise run release v0.8.0

scripts/release.sh checks Cargo.toml version, commits pending work, pushes branch, creates an annotated tag, and pushes the tag.

Manual equivalent:

git commit -m "..."
git tag -a v0.8.0 -m "zuko v0.8.0"
git push origin main v0.8.0

Linux zuko app bundle

x86_64 Linux release includes cage/ next to the binary:

  • cage
  • libwlroots-0.20.so
  • libliftoff.so.0
  • libseat.so.1
  • libxcb-errors.so.0

Built in release.yml from fedora:latest packages. aarch64 Linux does not bundle cage yet; users need cage on PATH.

iOS

Signed/TestFlight builds: ios/DISTRIBUTION.md.

Security

Model

  • Host identity: ~/.config/zuko/key.
  • Host ticket: endpointa…, sensitive dial information containing the host public key and current addresses.
  • Client allow-list: ~/.config/zuko/authorized_clients.
  • Client identity: a private local key used to derive a host-scoped token.
  • Saved connection state: ~/.config/zuko/hosts, iOS Keychain, or browser IndexedDB.

Connections are Iroh QUIC and end-to-end encrypted. Public relays see encrypted payloads but can observe connection metadata and traffic volume.

Current shell access requires both:

  1. enough ticket information to dial the host; and
  2. a client token present in the host’s allow-list.

The ticket alone does not authorize a shell. Keep it private anyway: it exposes reachability metadata, and storing all connection material defensively limits the effect of future protocol changes.

Trust boundaries

  • zuko share is the enrollment boundary. Anyone who obtains its short code while it is active can receive connection information and register a token.
  • The pairing code is memorable rather than high-entropy. Argon2id slows guessing, and the short timeout/count bound exposure; do not use --timeout 0 unattended.
  • The host’s config directory controls identity and authorization. Local access to those files is outside zuko’s remote threat boundary.
  • Losing a paired client exposes that client’s access until it is removed from the host allow-list.
  • Browser state is available to scripts on the same origin. The Labs web client should not be treated as a hardened client until it has a dedicated origin.

Rules

  • Protect saved connection state and client identity together.
  • Host tickets are handed out through zuko share/claim only.
  • zuko host never prints the raw ticket.
  • zuko share rejects stale current_ticket.
  • zuko host admits only tokens in authorized_clients.
  • Remove a lost client with zuko rm <name>; use zuko reset if trust cannot be narrowed safely.

Manage trust:

zuko ls
zuko rm <name>
zuko reset          # remove key/current_ticket, clear authorised clients
zuko reset --yes

After reset, restart host and re-pair clients.

Report vulnerabilities

Use GitHub Security Advisories: adonm/zuko/security/advisories/new.

Use private advisories for vulnerabilities.

Scope: src/, web/, wire protocol, handoff, service installer, and iOS app.

Contributing

Use mise; CI uses the same tasks.

mise install
mise run check       # fmt --check + clippy + tests + swiftlint
mise run test        # Rust clippy + unit tests
mise run test-e2e    # live Iroh network + PTY
mise run preflight   # CI-ish, includes iOS build
mise run build

iOS:

mise run setup-ios
mise run build-ios
swift test --package-path ios/ZukoWire

Before PR:

  • mise run check is green.
  • If iOS Swift/build config changed, run mise run preflight where possible.
  • Keep commits terse and imperative.
  • Update docs/protocol.md for wire changes.
  • Update docs/host.md for CLI/state changes.
  • Update docs/roadmap.md when a support tier, priority, or product boundary changes.
  • Run zuko doctor after service/ticket changes; it must remain read-only and avoid printing keys, tickets, or client tokens.

Scope new work

Read the roadmap and design principles first. Core reliability, recovery, diagnostics, and trust management take priority over new clients and streaming modes.

For a new platform, protocol, or background service, describe:

  • the Core user problem it solves;
  • its intended product tier;
  • its trust and resource boundaries;
  • how failure and recovery work;
  • the tests and ongoing maintenance it requires.

Client authors: start with protocol.md, then clients.md.

Security reports: use GitHub Security Advisories.