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 doctorchecks the whole install;--fixrepairs what it can (including retiring leftovers of the old claude-session-guard install). - What has it been doing?
unsnooze logs -ffollows 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 reaplists finished panes and empty sessions (dry-run);--yescloses them. - Leaving?
unsnooze uninstallremoves wrappers and hooks;--purgeremoves 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
claudestarts nothing watched. The shell wrapper lives in~/.zshrcor~/.bashrc(~/.config/fish/config.fishfor fish), so it only applies to shells started afterunsnooze setupran. Open a new terminal, then confirm withunsnooze 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— notcursor, which is the IDE launcher (cursor .) and must keep working, and not the neweragentalias, which is far too generic a name to shadow safely. Launch withcursor-agentand the session is tracked; on some machinesagentresolves 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 claudeorheadroom 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 whileguiWatchand that agent are enabled. For full pane monitoring with Headroom v0.34, first runheadroom install apply --scope provider --providers manual --target claude --target codex, then invoke normalclaude/codex, leaving Unsnooze as the outer launcher. - The limit hit but nothing was recorded. A detection problem. Either the
StopFailurehook is not installed (doctorreports it) or the banner wording was not recognised. Capture it withunsnooze 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 emptypremiumbucket 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. - 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 logsshows 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. If you declined it during setup, nothing runs while the terminal is closed;
doctorreports whether it is running and under which pid.
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.
Development
$ 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.