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.
$ 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
| state | meaning |
|---|---|
online | Fresh answer, latency shown in ms. |
stale | Host currently unreachable, showing its last-known sessions from cache (up to 24h, age shown). |
needs-auth | The 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. |
unreachable | ssh couldn't connect (timeout or connection failure). |
skew | Remote 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 <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 passwordprompt(default) — typed at run time, no-echo, on whichever terminal runsfleet/hosts test. Nothing is ever stored. Interactive-only by nature: under the daemon, in pipes, and in the dashboard's Fleet tab it showsneeds-authinstead of prompting.env— reads a variable you export (defaultUNSNOOZE_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 viasecurity find-generic-password(defaults: serviceunsnooze-<name>, account = the user part ofdest). 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.
$ 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 secretPer-OS --cmd recipes for the command source:
| OS | example --cmd |
|---|---|
| macOS | security find-generic-password -s <service> -a <account> -w |
| Linux | pass show <path> or secret-tool lookup <attr> <value> |
| Windows | powershell -Command "..." (e.g. wrapping Get-StoredCredential) or op read … |
| any OS | op 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
StrictHostKeyCheckingis never touched. - The remote is the authority.
unsnooze _remoteis 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 withcommand="unsnooze _remote",restrictin the remoteauthorized_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 ownSSH_ASKPASShook — 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/**.jsonltranscripts (session id, cwd, reset time) — shared by the CLI and the VS Code extension. - Codex writes a
rate_limitssnapshot (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 isolatedCLAUDE_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:
$ 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.