documentation

SSH multi-host fleet

One dashboard for every machine you code on — pulled over ssh, read-only by default — plus the GUI surfaces and platforms unsnooze can watch.

Multi-host fleet

See and act on unsnooze sessions on other machines from one terminal — over your own SSH, no new service to run. The remote host needs unsnooze installed; transport is your existing ~/.ssh/config, keys, and agent.

fleet
$ unsnooze hosts add work you@work-box.local   # register an ssh destination
$ unsnooze hosts list                          # registered hosts
$ unsnooze hosts test work                     # pre-flight: credential + reachability
$ unsnooze hosts rm work                       # forget a host
$ unsnooze fleet [--json]                      # every host's sessions, fanned out over ssh
$ unsnooze dashboard fleet                     # live Fleet tab

Before adding a host, connect to it the normal way once (ssh <host>) so OpenSSH pins the host key itself. unsnooze never weakens host-key checking to skip that step — an unknown or changed host fails fast instead of being silently trusted. If dest is omitted, the name doubles as the destination (so any ~/.ssh/config alias works as-is). Hosts live in ~/.unsnooze/hosts.json.

See and mark — not type remotely

The fleet view lists every reachable host's tracked sessions (state, reset countdown, attach hint). In the dashboard's Fleet tab, selecting a stopped remote session and pressing R (uppercase — lowercase r refreshes) only marks it due; C cancels it. The remote's own daemon does the actual keystrokes, under the same ownership/liveness/menu gates as a local session — a compromised viewer cannot make a remote type anything else. Stopped and resumed sessions print an attach hint (ssh -t <host> 'tmux new -A -s <session>' or zellij attach <session>) so you can hop over and watch. Note: remote resume/cancel always targets one session — --all is local-only.

Host states

statemeaning
onlineFresh answer, latency shown in ms.
staleHost currently unreachable, showing its last-known sessions from cache (up to 24h, age shown).
needs-authThe credential is the problem, not the network — a password source didn't resolve, ssh rejected the password, or a prompt host was tried without a terminal. Distinct from unreachable; fix the source and verify with hosts test.
unreachablessh couldn't connect (timeout or connection failure).
skewRemote unsnooze too old to speak the protocol — update it.

Hosts are polled in parallel (bounded pool, hard per-host timeouts around 8 s) so one dead box never blocks the rest; interactive password prompts run one at a time afterwards, with a generous timeout so you're never cut off mid-password. Like preview, unsnooze fleet exits 2 when some host has a stopped session — scriptable.

Auth: keys (default) or password

Every host picks its own auth. key is the default and stays the hardened path (ssh BatchMode, agent/keys, nothing new). --auth password adds four credential sources:

unsnooze hosts add — full syntax
$ unsnooze hosts add <name> <dest>
      [--auth key|password]                  # default: key
      [--source prompt|env|keychain|command] # default: prompt, once --auth password is set
      [--env <VARNAME>]                      # env source: var to read (default UNSNOOZE_PW_<NAME>)
      [--service <s> --account <a>]          # keychain source (macOS only)
      [--cmd '<command>']                    # command source: program whose stdout is the password
  • prompt (default) — typed at run time, no-echo, on whichever terminal runs fleet / hosts test. Nothing is ever stored. Interactive-only by nature: under the daemon, in pipes, and in the dashboard's Fleet tab it shows needs-auth instead of prompting.
  • env — reads a variable you export (default UNSNOOZE_PW_<NAME>). Daemon-capable. Positioned as a low-friction/CI convenience, not secret-manager-grade — your shell can already read it.
  • keychain — macOS-only built-in via security find-generic-password (defaults: service unsnooze-<name>, account = the user part of dest). Daemon-capable.
  • command — the portable, first-class source: any program whose stdout is the password. Daemon-capable, works on every OS. Its stderr is deliberately discarded so a chatty secret tool can never leak into logs.
examples
$ unsnooze hosts add laptop me@laptop.local --auth password
# → prompts (no-echo) every time it's used from a real terminal

$ unsnooze hosts add gpu ubuntu@gpu.example.com --auth password --source env --env UNSNOOZE_PW_GPU
# → export UNSNOOZE_PW_GPU=... first

$ unsnooze hosts add mac me@mac-mini.local --auth password --source keychain --service unsnooze-mac --account me
# → macOS only; reads via `security find-generic-password`

$ unsnooze hosts add ci ci@build.example.com --auth password --source command --cmd 'op read op://vault/ci/password'
# → any OS; runs your secret manager, reads its stdout

$ unsnooze hosts test gpu
# → resolves the credential + probes reachability; prints "auth ok" or a
#   hint ("needs-setup: ..."), never the secret

Per-OS --cmd recipes for the command source:

OSexample --cmd
macOSsecurity find-generic-password -s <service> -a <account> -w
Linuxpass show <path> or secret-tool lookup <attr> <value>
Windowspowershell -Command "..." (e.g. wrapping Get-StoredCredential) or op read …
any OSop read op://vault/item/password (1Password CLI) — or any manager's read command

Requirements & limits: stored sources (env/keychain/command) need OpenSSH ≥ 8.4 (SSH_ASKPASS_REQUIRE) — older ssh shows needs-auth. On native Windows, ssh.exe prompts fine for an interactive prompt host, but stored sources need Git-for-Windows or WSL ssh for now. keychain exists only on macOS — Linux and Windows use --source command with the recipes above.

Fleet security posture

  • No new network surface. No listening ports, no custom auth, no tokens — only outbound OpenSSH child processes, and StrictHostKeyChecking is never touched.
  • The remote is the authority. unsnooze _remote is the single remote entrypoint (status/resume/cancel); resume only marks a session due — the remote daemon types under its own gates. Lock a key to it with command="unsnooze _remote",restrict in the remote authorized_keys — that key can never open a shell.
  • Remote output is untrusted. Every field a host returns is control-character-stripped, length-capped, and copied into fresh objects before it touches your terminal or state; session names are re-validated before being shown in a copy-pasteable attach hint.
  • Passwords never touch argv, ps, or unsnooze's environment. They flow through OpenSSH's own SSH_ASKPASS hook — helper stdout to ssh, in-process, for that one ssh child. unsnooze stores no plaintext: keychain/command delegate to the OS store or your manager.

GUI surfaces

Sessions in Claude Code's VS Code extension, the ChatGPT desktop app, and Claude desktop have no pane to scrape. unsnooze daemon tails the session files those surfaces already write:

  • Claude Code records every rate-limit stop as a structured entry in its ~/.claude/projects/**.jsonl transcripts (session id, cwd, reset time) — shared by the CLI and the VS Code extension.
  • Codex writes a rate_limits snapshot (usage %, exact epoch reset time) into every rollout under ~/.codex/sessions/ — shared by the CLI, IDE extension, and the ChatGPT desktop app. Where Codex lives only inside ChatGPT.app, unsnooze resumes through the app-bundled binary.
  • Claude desktop (cowork) sessions (experimental, macOS) run in sandboxes under ~/Library/Application Support/Claude; revival uses the session's isolated CLAUDE_CONFIG_DIR.

At reset the session revives in a multiplexer pane with claude --resume <id> / codex resume <id> — same session file, so the conversation stays visible in the GUI's own history. Enable in unsnooze setup or with unsnooze install --daemon; disable with unsnooze config set guiWatch off.

Platforms

macOS / Linux: install tmux or Zellij (brew install tmux, brew install zellij). In auto mode unsnooze uses the multiplexer you're inside; pin one with unsnooze config set multiplexer tmux.

Windows: unsnooze runs inside WSL — where the agent CLIs live on Windows anyway:

WSL (Ubuntu etc.)
$ sudo apt install tmux        # or install Zellij
$ npm install -g unsnooze && unsnooze setup

Desktop notifications inside WSL arrive as native Windows toasts through powershell.exe — no X server needed. Native Windows without WSL is not supported: with no tmux or Zellij there is no pane to watch, and unsnooze says so and runs your CLI unwatched instead of breaking it.