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
Read next
- Direction and roadmap
- Host & CLI
zuko app(Labs)- Wire protocol
- Client notes
- Browser target notes
- Releasing
- Security
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:
- install a per-user host service;
- pair a device with a one-time code;
- reconnect by a memorable name;
- survive brief network changes;
- 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
| Tier | Meaning | Current surfaces |
|---|---|---|
| Core | Primary maintained and release-gated workflow | Linux/macOS host and Rust CLI |
| Beta | Intended for regular use, but availability or compatibility is not yet stable | iOS/iPadOS client |
| Labs | Opt-in experiments used to learn; APIs and behavior may change | Browser 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 doctorpath 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, orscreen); - 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:
- prevent unauthorized shell access or secret loss;
- preserve shell correctness and recoverability;
- improve pairing, diagnostics, and trust management;
- improve resource use and maintainability;
- 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
| Path | Role |
|---|---|
~/.config/zuko/key | host identity |
~/.config/zuko/current_ticket | live dial ticket, refreshed by host |
~/.config/zuko/authorized_clients | host allow-list |
~/.config/zuko/hosts | client-side saved hosts |
~/.config/zuko/client_key | CLI 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:
| Flag | Default |
|---|---|
--prefix | ~/.local |
--key | ~/.config/zuko/key |
--shell | $SHELL |
--no-start | disabled |
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, orscreenfor 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:
| Flag | Default | Notes |
|---|---|---|
--ticket | current_ticket | advanced override; argv may expose the ticket, so prefer the file |
--label | hostname | default save name on client |
--count | 1 | accepted claims before exit |
--timeout | 300 | seconds; 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
| Flag | Default | Notes |
|---|---|---|
--list | — | list aliases |
--dry-run | — | print launch command/env |
--test-pattern | — | draw Kitty test image; no cage |
--doctor | — | check cage/protocol/geometry |
--debug-child | — | let child stdout/stderr through |
--no-sandbox | — | browser/container escape hatch |
--no-cursor | — | hide crosshair cursor overlay |
--fps | 30 | max frame rate |
--max-mbps | 80 | approximate graphics bandwidth cap; 0 disables |
--graphics-codec | auto | auto, png, rgb |
--scale | 1.0 | render below/above terminal pixel size |
--software | — | force 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:
<exe_dir>/cage/~/.local/share/zuko/cagePATH$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
| Type | Name | Direction | Payload |
|---|---|---|---|
0x00 | DATA | both | terminal bytes |
0x01 | RESIZE | client → host | cols:u16 rows:u16 pixel_width:u16 pixel_height:u16 |
0x04 | PING | both | nonce:u64 |
0x05 | PONG | both | nonce:u64 |
0x06 | ATTACH | client → host | token:16 bytes + resize payload |
0x07 | ATTACHED | host → client | token:16 bytes |
0x08 | AUTHORIZE | client → handoff host | token:16 bytes + UTF-8 label |
0x09 | ERROR | host → client | code: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
- Client dials host ticket on
zuko/2. - Client opens data bidi stream.
- First frame must be
ATTACHwith a non-zero host-scoped token and current terminal size. - Host checks
authorized_clientsfor the token. - Host creates or reattaches the PTY keyed by that token.
- 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:
DATAclient → host. - PTY output:
DATAhost → client. - Size changes:
RESIZEon control stream when available, otherwise data stream. PINGreplies withPONGcarrying 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
ERRORand 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):
- Generate memorable code.
- Derive throwaway Iroh secret:
Argon2id(normalized_code, salt="zuko-share-handoff-v1") -> 32-byte seed. - Bind endpoint on
zuko/handoff/1. - Open uni stream and write:
<label>\n<ticket> - Wait briefly for client
AUTHORIZEuni stream. - Save token + label to
authorized_clients.
Client (zuko claim / zuko <code>):
- Derive same throwaway endpoint id from code.
- Dial
zuko/handoff/1, retrying until timeout. - Read label + ticket.
- Parse ticket host id.
- Derive stable host-scoped token from local client secret + host id.
- Open uni stream, send
AUTHORIZE(token, client_label). - 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:
| Client | Status | Source |
|---|---|---|
| Rust CLI | Core | Linux/macOS release binary; src/client.rs |
| iOS/iPadOS | Beta | iOS/iPadOS 26.5; ios/Zuko/ |
| Web | Labs | Open 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:
- Claim ticket via
zuko/handoff/1:- derive handoff key from code;
- dial throwaway endpoint;
- read
<label>\n<ticket>; - derive host-scoped token;
- send
AUTHORIZEbefore closing handoff connection.
- Persist a client secret. Derive non-zero token from
(client secret, host id). - Dial host ticket with ALPN
zuko/2. open_bi; first frame must beATTACH(token, cols, rows, pixels).- Pump length-prefixed frames:
- stdin/terminal bytes →
DATA; - remote
DATA→ terminal emulator; - size changes →
RESIZE; - optional control stream for
RESIZE/PING/PONG.
- stdin/terminal bytes →
- Store
ATTACHEDtoken. Reuse it for short reconnects. - Surface
ERRORas 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:
- explicit authorization and safe local secret storage;
- correct PTY behavior and bounded failure modes;
- actionable operator feedback and recovery;
- protocol simplicity and compatibility;
- 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, orscreen. - 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,PONGon an optional control stream, with data-stream fallback; - handoff ALPN:
zuko/handoff/1; - authorization:
AUTHORIZEduring handoff, enforced onATTACH.
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:
wtermwith@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 bymise run build-webintotarget/book/web/. - CI publish:
.github/workflows/docs.ymlbuilds 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/ghosttyrenders 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 appKitty 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:
cagelibwlroots-0.20.solibliftoff.so.0libseat.so.1libxcb-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:
- enough ticket information to dial the host; and
- 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 shareis 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 0unattended. - 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/claimonly. zuko hostnever prints the raw ticket.zuko sharerejects stalecurrent_ticket.zuko hostadmits only tokens inauthorized_clients.- Remove a lost client with
zuko rm <name>; usezuko resetif 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 checkis green.- If iOS Swift/build config changed, run
mise run preflightwhere possible. - Keep commits terse and imperative.
- Update
docs/protocol.mdfor wire changes. - Update
docs/host.mdfor CLI/state changes. - Update
docs/roadmap.mdwhen a support tier, priority, or product boundary changes. - Run
zuko doctorafter 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.