documentation

Troubleshooting and security

When a wake did not happen, start with unsnooze doctor. The security model and the local dev loop follow.

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.

Start with the symptom

Almost every report falls into one of these. The distinction that matters most is whether the session was recorded at all — that separates a detection problem from a wake problem, and they have different fixes.

  • Typing claude starts nothing watched. The shell wrapper lives in ~/.zshrc or ~/.bashrc (~/.config/fish/config.fish for fish, PowerShell's $PROFILE on Windows), so it only applies to shells started after unsnooze setup ran. Open a new terminal, then confirm with unsnooze doctor. Nothing is protected until the wrapper is loaded, because the wrapper is the entry point — you never invoke unsnooze directly.
  • Cursor sessions are not watched, even with the agent enabled. unsnooze wraps cursor-agent — not cursor, which is the IDE launcher (cursor .) and must keep working, and not the newer agent alias, which is far too generic a name to shadow safely. Launch with cursor-agent and the session is tracked; on some machines agent resolves to another vendor's CLI entirely.
  • Cursor hit its limit and unsnooze scheduled no wake. Deliberate. Cursor's usage resets on your monthly billing cycle, not a rolling window, so a scheduled wake would sleep for weeks and then type into a wall that never moved. The stop is recorded as a model limit: you are notified that it needs a decision, and unsnooze probes at 15/30/60 minutes and resumes the moment the banner clears — when you change plan, or the cycle rolls.
  • Using headroom wrap claude or headroom wrap codex. Headroom resolves and launches the real executable directly, bypassing shell functions, so Unsnooze cannot attach its same-pane monitor. The Claude hook can still record stops; the daemon file watcher can too while guiWatch and that agent are enabled. For full pane monitoring with Headroom v0.34, first run headroom install apply --scope provider --providers manual --target claude --target codex, then invoke normal claude / codex, leaving Unsnooze as the outer launcher.
  • The limit hit but nothing was recorded. A detection problem. Either the StopFailure hook is not installed (doctor reports it) or the banner wording was not recognised. Capture it with unsnooze report — an unmatched banner is a one-release fix, but only if someone sends the text.
  • Codex stopped at 99% with no tracked session. Update to v1.19.0 or later and check that the daemon, guiWatch, and the Codex agent are enabled. The watcher now recognizes a recent 99% five-hour snapshot followed by an empty premium bucket with no credits in the same rollout, then schedules the session using the previous reset time. A 99% reading alone is not a stop signal.
  • Codex behind a proxy never stopped. Through an OpenAI-compatible proxy the rate-limit headers never reach Codex, so its snapshots are empty. From v1.19.1 unsnooze also reads the limit error Codex writes when the turn fails (codex-cli 0.145 or later) and dates the stop from its banner.
  • A headless revival died. Without a multiplexer (native Windows, a bare server) a revival that exits non-zero is retried, and unsnooze status shows its own last words as the last error — spawn codex ENOENT means the daemon cannot find the agent. unsnooze doctor prints the binary each agent resolves to. On Windows the daemon keeps the PATH it started with: set UNSNOOZE_CODEX_BIN (or UNSNOOZE_CLAUDE_BIN, …) to the agent's .exe, or restart the daemon with unsnooze install --daemon after an agent update.
  • It was recorded but never woke. A wake problem, and unsnooze preview <id> names the reason rather than guessing. The usual answers are a guard deliberately holding it — see guards — or the five-attempt cap having been reached.
  • It woke, but at the wrong time. Read the provenance in unsnooze status: a reset shown as (absolute, from hook) came from the agent itself, while (absolute, from scrape) was read off the pane. A reset that looks hours out is a parsing bug worth reporting with the banner text attached — reset times are always absolute, never a relative countdown.
  • It resumed while still rate-limited. Expected and handled: the pane is re-captured after every wake, and if the banner is still there unsnooze reschedules from the fresh one. Overload is not a limit, so a transient overload message is not treated as one.
  • You upgraded, and the fix did not take. A monitor is started once, when the agent launches, and it watches that pane for as long as the agent lives — days, in a long tmux session. Upgrading replaces the package on disk but cannot reach into a process that already loaded the old code. From the version after 1.14.2 both the monitor and the resumer notice the package changed underneath them and hand off to a replacement on their own, so an upgrade propagates within seconds. A session that was launched under 1.14.2 or earlier predates that machinery and needs one restart: exit the agent in that pane and start it again. unsnooze logs shows which version each watcher is running from the moment it hands off.
  • The machine was asleep at reset time. Wakes are dispatched by the daemon — a launchd agent on macOS, a systemd user unit on Linux, an on-demand process on Windows. If you declined it during setup, nothing runs while the terminal is closed; doctor reports whether it is running and under which pid.

Common questions

What does "You've hit your usage limit" mean?

Claude and ChatGPT plans meter usage in a rolling 5-hour window plus a weekly cap. When either runs out, the agent stops mid-task and shows a banner with the reset time. Nothing is lost: the session can be resumed once the limit resets (claude --resume <id>, codex resume <id>). unsnooze does that automatically, for every stopped session — typing into the live pane when it is still open, reopening it when it is not.

Does this get around the rate limit?

No. unsnooze waits for the reset exactly like you would, resumes once, and checks the limit actually lifted. It replaces the 4am alarm, not the limit.

Does it work if my laptop was asleep or the terminal was closed?

Yes. Reset times are stored as absolute timestamps and checked every 30 seconds instead of one long timer, so a laptop that slept through the reset resumes on the next check, and weekly limits are just a later timestamp. Dead panes are reopened by session id. State writes go through a lock and an atomic rename, so several sessions stopping at once cannot corrupt the ledger; a corrupt file is set aside, never fatal.

Why did resuming a big session use so much quota?

Prompt-cache expiry. After hours stopped, the provider's cache is gone, so the first message — unsnooze's or a hand-typed "continue", same cost — re-reads the whole conversation at full price. /compact before the limit helps, and contextGuard tells you (or holds the session) when a wake will be expensive.

How do I update?

unsnooze update (or npm i -g unsnooze). unsnooze checks npm at most once a day and tells you when a newer version exists; after updating, the next command shows what's new. Turn the check off with unsnooze config set updateCheck off.

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-approve, no touching MCP config — unsnooze adds no permission flags of its own (flags you set in resumeExtraArgs are yours); your agent's own permission model governs everything after the wake. Headless Codex revivals pass --skip-git-repo-check, since codex exec refuses non-git folders the session already ran in.
  • Nearly zero network: one daily version check, plus ntfy only if you configure it. Zero telemetry; state stays owner-only (0700/0600) 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.

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
$ node scripts/e2e-herdr.mjs    # drive the herdr backend against a real herdr (not in CI)
$ 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.