documentation

unsnooze docs

Everything below is drawn from the shipped CLI — command output, defaults, and behavior as implemented. For the threat model, read SECURITY.md.

Getting started

You need Node ≥ 20 and tmux ≥ 3.2 or Zellij, on macOS, Linux, or Windows via WSL.

install
$ npm install -g unsnooze
$ unsnooze setup

The setup wizard asks which agents to guard and which toggles you want, then wires everything up:

  • Shell wrappers into ~/.zshrc / ~/.bashrc — after this, typing claude or codex runs the CLI inside a watched multiplexer pane. You never call unsnooze directly to be protected.
  • The Claude StopFailure hook — the authoritative limit-stop signal, carrying the session id.
  • Optionally the daemon (a launchd agent on macOS, a systemd user unit on Linux) for GUI-surface watching and pre-wall usage warnings.
  • ~/.unsnooze/config.json with your choices — see Settings.

Every file it touches is backed up first (*.unsnooze-orig pristine, *.unsnooze-bak rolling), and unsnooze uninstall removes every change. Verify the install any time with unsnooze doctor — it reports problems, not a checklist, so a healthy install is a one-liner:

unsnooze doctor
$ unsnooze doctor
unsnooze doctor: all clear — install is healthy.
  · resumer/daemon: running (pid 4821)

Anything wrong comes back as a finding with a suggested fix, and doctor --fix repairs what it safely can. One nuance on requirements: tmux version isn't checked — any tmux runs, but reviving dead sessions uses env flags that need tmux ≥ 3.2.

Day to day

Run your agents like always. When one hits its limit, unsnooze records the stop in ~/.unsnooze/state.json — agent, session id, working directory, pane, and the reset time parsed from the banner — and wakes it when the limit lifts.

unsnooze status (plain / piped)
$ unsnooze status
  [STOPPED  ] f3a1c2d4  claude 5h      /Users/you/work/payments
              mux tmux · pane %12 · session unsnooze · via hook · resets 19/7/2026, 3:00:00 am (2h 41m) (absolute, from hook) · attempts 0/5 · ctx ~152k tok
  [RESUMED  ] 92d6f63d  claude 5h      /Users/you/oss/unsnooze
              mux tmux · pane %7 · session unsnooze · via cli · resets 18/7/2026, 4:11:00 pm (due now) (absolute, from scrape) · attempts 1/5 · attach: tmux attach -t unsnooze

Each session is two lines: a header (status, 8-char id, agent, limit window, working directory) and a detail line (pane, how the stop was detected, the reset time with countdown and its provenance, attempts out of 5, plus ctx ~152k tok on stopped sessions and an attach hint on live ones). Statuses are stopped, resuming, resumed, cancelled, and failed.

On an interactive terminal, status, usage, and sessions open the live dashboard instead; pipes, CI, NO_COLOR, and --json stay plain. The common interventions:

interventions
$ unsnooze message f3a1 "Run the tests first, then continue."
$ unsnooze resume-now f3a1     # don't wait for the reset time
$ unsnooze cancel --all        # stop tracking everything

Not sure what it's about to do? unsnooze preview is a true dry-run: it prints exactly what would be typed, where, and why — or what's holding it back — and sends nothing. It shares its decision code with the real dispatcher, so it cannot drift from what dispatch actually does. It exits 2 when a wake is actionable right now, 0 otherwise — scriptable.

Command reference

Verbatim from unsnooze help (current main):

unsnooze help
unsnooze — wakes every limit-stopped AI coding session when the limit resets

Usage:
  unsnooze [claude args...]        run claude under limit-watch (default)
  unsnooze _run <agent> [args...]  run a specific agent CLI under limit-watch
  unsnooze status                  list tracked sessions + reset countdowns
  unsnooze resume-now [id|--all]   resume stopped session(s) immediately
  unsnooze cancel [id|--all]       stop tracking session(s)
  unsnooze message <id|--all> <t>  set a per-session wake message (--clear to reset)
  unsnooze sessions                list unsnooze-owned mux sessions + panes
  unsnooze reap [--dry-run|--yes]  close terminal-record panes / empty sessions
                                   (default: dry-run; pass --yes to apply)
  unsnooze doctor [--fix]          check install health; find (and with --fix
                                   retire) leftovers of the old
                                   claude-session-guard install
  unsnooze preview [id]            dry-run: what WOULD happen right now, and
                                   why — nothing is typed or opened (exit 2
                                   when a wake is actionable, else 0)
  unsnooze dashboard [tab]         live TUI (status|usage|sessions|doctor|logs|fleet|prompts)
                                   — q to quit, mouse: click/wheel (m toggles)
  unsnooze hosts [add|rm|list]     register ssh hosts for the fleet view
                                   add <name> <dest> [--auth key|password]
                                     [--source prompt|env|keychain|command]
                                     [--env VAR] [--service s --account a]
                                     [--cmd '<command>']
  unsnooze hosts test <name>       pre-flight a host: resolves its credential
                                   and probes reachability (secret never shown)
  unsnooze fleet [--json]          all hosts' sessions (hosts add <name> first)
  unsnooze prompt add [text...]    queue a prompt to start a NEW session in a
                                   project when the limit resets ([--agent id]
                                   [--project path] [--at time|--now])
  unsnooze prompt list [--json]    list queued prompts
  unsnooze prompt remove <id>      cancel a queued prompt
  unsnooze prompt clear            cancel all pending queued prompts
  unsnooze usage [--json]          account burn rate & time-to-limit forecast
                                   (--install-statusline for exact Claude %,
                                    --uninstall-statusline to remove it)
  unsnooze logs [-f]               show (or follow) the unsnooze log
  unsnooze update                  update unsnooze itself to the latest version
  unsnooze daemon                  persistent watcher for GUI sessions (VS Code
                                   extension, desktop apps) — no live pane needed
                                   to detect; revival opens in tmux or Zellij
  unsnooze config [list|get|set]   view or change settings (toggles, global +
                                   per-agent resume messages, notifyChannel
                                   auto|native|osc|bell, updateCheck)
  unsnooze setup                   interactive setup wizard (agents + toggles)
  unsnooze install [--yes]         wire up shell wrappers + hooks (non-interactive)
  unsnooze uninstall [--purge]     remove wrappers + hooks (and state with --purge)
  unsnooze report [agent] [pane]   capture a pane to report an undetected banner
  unsnooze help                    show this help (also -h / --help)

Settings

unsnooze setup writes ~/.unsnooze/config.json; change anything later with unsnooze config set <key> <value>. The full surface, with defaults:

keydefaultmeaning
multiplexerautoauto, tmux, or zellij. Auto prefers the multiplexer you're inside, then the only installed backend, tmux as tie-breaker.
autoResumetrueMaster switch. Off = stops are still tracked, but nothing resumes until resume-now or turning it back on.
menuAutoAnswertrueMay unsnooze answer Claude's limit menu (send keys in your pane)? Off = watch-only.
notificationstrueMaster switch for all notifications. Off silences every channel.
notifyChannelautoauto, native, osc, or bell — see Notifications.
guiWatchtrueMay the daemon watch session files for GUI-surface stops? Needs the daemon running.
resumeMessage"Continue where you left off…"The message typed to wake a session. Override per session with unsnooze message <id>.
resumeMessages.<agent>""Per-agent override of resumeMessage (.claude, .codex, .grok, .qwen, .kimi, .opencode, .agy). Empty = global message.
agents.claude / agents.codextrueWhich CLIs are guarded.
agents.grokagents.agyfalseExperimental adapters — off by default; enable in setup or e.g. config set agents.qwen on.
workspaceGuardinformRepo changed while a session slept? inform wakes it with a heads-up; pause holds it; off disables. See Guards.
contextGuardinformBig cold context at wake? inform resumes and notifies; pause holds sessions above the threshold; off disables. Claude Code only for now.
contextGuardTokens100000Context-size threshold (tokens) for contextGuard.
usageWarnnotifyPre-wall usage warnings from the daemon: notify or off.
usageWarnAt80,95Percent thresholds for usage warnings. Non-numeric values fall back to the default — never silently disable.
mousetrueMouse support in the dashboard; toggle live with m. Hold Shift (Option in iTerm2) to select text.
reapResumedfalseOpt-in: auto-close resumed panes idle longer than reapIdleAfter.
reapIdleAfter604800000 (7d)Idle age (ms) before an opt-in auto-reap closes a resumed pane.
updateChecktrueDaily new-version check — a plain GET to the npm registry, nothing identifying.
ntfyTopic / ntfyServer / ntfyToken / ntfyPrivacy"" / ntfy.sh / "" / fullPhone push via ntfy — off until a topic is set. See Notifications.
remoteQueuetrueSet on the host being controlled: may other hosts queue prompts on this one (prompt add --host <this>)? Off = the queue verbs answer a typed disabled instead of silently dropping. See Queued prompts.

Every setting also has an UNSNOOZE_* environment override (src/settings.js), and timings/paths are tunable via UNSNOOZE_* vars (src/config.js).

Multiplexer session names

Interactive launches own the base session name (default unsnooze); a second concurrent terminal takes unsnooze-2, and so on. The resumer daemon may join a live session but only ever creates unsnooze-resumed — a revived agent never steals the interactive name. Override with UNSNOOZE_SESSION_NAME and UNSNOOZE_RESUME_SESSION; attach with e.g. tmux attach -t unsnooze-resumed.

Usage forecast

Recovery is half the job; unsnooze usage is the other half — knowing when the wall is coming so you can /compact, pause, or switch models first.

unsnooze usage
$ unsnooze usage
unsnooze usage — account burn & time-to-limit  (daemon: running · warnings at 80,95%)

  claude  5h      [█████████████░░░░░░░]  ~64%  (calibrated from 4 stops)
          burn    ~31k weighted tok/min over last 42 active min
          wall    ~1h 10m at this pace · window resets 8:00 pm (absolute)

  codex   5h      [███░░░░░░░░░░░░░░░░░]  5% used  (exact)
          monthly [██░░░░░░░░░░░░░░░░░░]  5% used  (exact) · resets Aug 11
          burn    idle — no active burn

Every figure carries its provenance — never a bare percentage:

  • (exact) — Codex always (local used_percent + epoch reset). Claude only with the opt-in statusline shim, which persists Claude Code's server-authoritative rate limits.
  • (calibrated from N stops) — Claude token burn against a ceiling learned from your recorded limit stops. Not plan presets.
  • (estimated — calibrating, needs one observed limit stop) — used tokens + burn shown; the ceiling stays unknown until the first observed stop.
statusline shim (opt-in)
$ unsnooze usage --install-statusline    # exact Claude % (chains your statusLine)
$ unsnooze usage --uninstall-statusline  # restore your original statusLine

With the daemon running, warnings fire at the usageWarnAt percent bands and at 30 / 10 minutes to the wall at your current pace, deduped once per window. Warnings may suggest /compact; unsnooze never auto-types it. usage --json emits a stable machine shape and exits 2 past the warn threshold — useful in scripts and statuslines.

Honest limits: Claude transcript sums are a lower bound — subscription quotas are account-pooled with claude.ai and the desktop app. Without the shim, Claude tops out at calibrated/estimated.

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.

Queued prompts

Queue a prompt now; unsnooze types it into a brand-new agent session — a fresh window in a project directory — once a usage limit clears (or at a time you choose). It's one-shot: each entry is delivered at most once.

prompt queue
$ unsnooze prompt add "run the full test suite and fix any failures"
# → interactive agent picker on a TTY; queued for next-reset in the cwd

$ unsnooze prompt add --agent codex --project ~/code/api --now "ship the release"
$ unsnooze prompt add --at "+2h30m" "rebase onto main"   # relative duration
$ unsnooze prompt add --at "9pm" "nightly cleanup pass"  # next occurrence of a clock time
$ unsnooze prompt add --at 1755000000 "..."              # epoch (seconds or ms), or an ISO-8601 timestamp

$ unsnooze prompt list [--json]   # id, agent, due, status, cwd, prompt preview
$ unsnooze prompt remove <id>     # cancel one pending/launching entry
$ unsnooze prompt clear           # cancel every pending/launching entry

Modes

  • next-reset (default) — delivered once no future reset time is known for that agent, i.e. once the current limit clears. If unsnooze holds no reset signal at all for that agent when you add the entry, there's nothing to wait on — it delivers on the very next daemon tick, and prompt add prints a notice to that effect right away. Use --at if you want a specific time instead.
  • --now — deliver on the next daemon tick, no reset wait at all.
  • --at <time> — deliver at a specific time: epoch (seconds or milliseconds), an ISO-8601 timestamp, a +2h30m-style relative duration, or a bare clock time (9pm, 2:05pm, 14:30) rolled to its next local occurrence.

If a delivery attempt lands on a pane that's still limited — the reset hadn't actually cleared, or a fresh --now/--at session hits the wall immediately — the entry goes back to pending behind a backoff floor before it's retried. That floor applies to every mode, so a failing --now/--at entry can't burn through every retry in the first few seconds. Verified delivery (capped at 5 attempts, same as resume) marks it failed and sends a notification.

autoResume does not gate prompt delivery — the queue runs independently of session tracking, even with autoResume off.

Fleet

--host <name> queues on a registered host instead of locally. --project (an absolute remote path) and --agent are both required — there's no local cwd to default to and no interactive picker over an ssh round-trip. The remote host re-validates everything server-side. Delivery feedback comes from unsnooze fleet / the dashboard's Fleet tab (per-host queued count) and that host's own notifications (ntfy reaches your phone from any host, not just the one you're sitting at). A host can refuse all queue traffic with remoteQueue: false (unsnooze config set remoteQueue off, env UNSNOOZE_REMOTE_QUEUE=0, set on the host being controlled) — the queue verbs then answer a typed "disabled" instead of silently dropping; a remote that predates this feature reports a clear "too old" error instead of failing silently.

fleet prompt queue
$ unsnooze prompt add --host gpu-box --project /home/me/repo --agent claude --now "..."
$ unsnooze prompt list --host gpu-box

Dashboard

The Prompts tab (7) lists queued entries; a opens an add form (path → agent → when — with a time prompt if you pick "at" — → a host step if you have hosts registered → prompt text), d/x removes the selected entry. The Status tab shows a <n> prompt(s) queued — tab 7 hint whenever entries are pending or launching.

Notifications

On limit-hit, resumed, and gave-up, unsnooze alerts you via the channel set in notifyChannel:

channelbehavior
autoOSC (when the terminal supports it) plus BEL on the pane tty; falls back to native only if OSC delivered nothing. No pane / non-tmux mux → native.
nativeOS toast — macOS osascript, Linux notify-send, WSL/Windows PowerShell toast.
oscForce OSC to attached client ttys; native if zero deliveries.
bellBEL to the pane tty; native if undeliverable.

OSC support: iTerm2, kitty, WezTerm, Ghostty, and Warp get OSC 9; rxvt gets OSC 777; Apple Terminal, VS Code, Alacritty, and Zed are denylisted (native is used instead). OSC/BEL need tmux's client/pane tty APIs — under Zellij, notifications fall back to native.

Phone push via ntfy

ntfy
$ unsnooze config set ntfyTopic "unsnooze-$(openssl rand -hex 8)"
$ unsnooze config set ntfyPrivacy terse   # keep paths out of push bodies

⚠ ntfy.sh topics are public — the name is the password. Use an unguessable topic like the generated one above, a tk_… access token (ntfyToken), or a self-hosted server (ntfyServer). Pushes fire alongside the local channel.

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.

Guards

workspaceGuard — the repo changed while it slept

The repo's HEAD and dirty state are fingerprinted at stop time and re-checked at wake. inform (default) resumes with a heads-up appended to the wake message — "Heads up: this workspace changed while the session was stopped (HEAD abc1234 → def5678). Re-read the current state of the repo before continuing." — while pause holds the session and resume-now shows the diff stat first; off disables.

contextGuard — the cold-cache wake tax

Providers cache your session's context, but that cache lives minutes, not hours. After a long limit stop, the first wake message — unsnooze's or a hand-typed "continue", identical cost — re-reads the entire conversation at full uncached price. A ~150k-token session can eat a real slice of the fresh window the moment it wakes.

unsnooze estimates the size from the session transcript (shown as ctx ~152k tok in status). inform resumes and notifies you of the price; pause holds sessions above contextGuardTokens (default 100k) for a manual resume-now. What actually helps: /compact before the wall, and lean overnight sessions.

Overload is not a limit

Transient 5xx/529/429 errors take a seconds-scale backoff path ([30, 60, 120, 240, 300]s ± jitter) and never enter the ledger.

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.

Security model

unsnooze is a scheduler that presses your keys — not an auto-approver. The short version of the contract:

  • Keys are typed only after proving the pane is yours — identity (ownership stamp or process-id + birth-time lease; pane ids get recycled, so a mismatch vetoes) and liveness (your agent foreground, not mid-stream). Unprovable → a fresh session is opened instead of typing.
  • Claude's limit menu is read before any key is sent; unreadable → nothing is pressed. It will never select "Upgrade your plan."
  • No --dangerously-skip-permissions, no auto-trust, no auto-approve, no touching MCP config — your agent's own permission model governs everything after the wake.
  • Nearly zero network: one daily version check, plus ntfy only if you configure it. Zero telemetry; state stays in ~/.unsnooze.
  • Releases are published to npm by CI with provenance.

Honest limits: unsnooze does inject keystrokes into your live terminal, and it does not sandbox your agent or defend against prompt injection — that's your agent's job. Full threat model and vulnerability reporting: SECURITY.md.

Troubleshooting

  • Something looks off? unsnooze doctor checks the whole install; --fix repairs what it can (including retiring leftovers of the old claude-session-guard install).
  • What has it been doing? unsnooze logs -f follows the log live; the dashboard's logs tab scrolls back with the mouse wheel.
  • A wake didn't happen? unsnooze preview <id> tells you exactly what's holding it back. After every real wake the pane is re-captured; if the limit banner reappears, unsnooze reschedules from the fresh banner, capped at five attempts.
  • A banner wasn't detected? unsnooze report [agent] captures the pane so you can paste it into an issue — that's how the experimental adapters get good.
  • Panes piling up? unsnooze reap lists finished panes and empty sessions (dry-run); --yes closes them.
  • Leaving? unsnooze uninstall removes wrappers and hooks; --purge removes state too.

Development

dev loop
$ npm test                      # unit tests (node:test)
$ ./scripts/e2e-simulate.sh     # full detect → wait → re-open cycle in a
                                # scratch tmux session (no real limits needed)
$ bash -n scripts/e2e-zellij.sh # syntax-check the Zellij smoke test
$ vhs demo/demo.tape            # regenerate the demo gif (brew install vhs)

Releases are tagged (git tag v<version>, then git push origin v<version>) and published to npm by CI with provenance via trusted publishing — see .github/workflows/release.yml. Contributions: open an issue first for anything behavioral; adapter banner captures (unsnooze report) are always welcome.