[BUG] ScheduleWakeup persists after Ctrl+C, daemon auto-respawns loop unattended causing unbounded token spend
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When a skill using /loop internally calls ScheduleWakeup, pressing Ctrl+C to interrupt the session does NOT cancel the pending wakeup timer. The Claude Code daemon auto-respawns the session and continues executing loop iterations indefinitely — even with the terminal closed and the Mac lid shut. This resulted in ~$300 of unintended API usage over a single weekend with no way to detect or stop it from the CLI.
What Should Happen?
Ctrl+C should cancel any pending ScheduleWakeup timers associated with the current session. The loop should stop when the user interrupts. There should also be a CLI command to list and kill active background jobs from any terminal.
Error Messages/Logs
~/.claude/jobs/e3b9dac0/state.json after Ctrl+C:
{"state": "working", "detail": "CDP triage complete (0 new bugs); next cycle 10:01", "tempo": "active", "inFlight": {"tasks": 2, "queued": 0, "kinds": ["session_cron"]}}
~/.claude/daemon/roster.json showed worker respawned with new PID after killing it:
{"attempt": 2, "pid": 35341, ...} — supervisor restarted it on exit 143 (SIGTERM)
CronList → empty. TaskList → empty. No CLI way to detect the running loop.
Steps to Reproduce
---
Steps to Reproduce
- Run any skill via
/loopwith a short interval (e.g./loop 5m /my-skill) - Press Ctrl+C to stop — you expect the loop to be cancelled
- Close the terminal window
- Check ~/.claude/daemon/roster.json — the worker entry persists
- The daemon continues running loop iterations unattended indefinitely
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.160
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Enterprise plan ($3,000/month limit). Loop ran ~864 iterations over a weekend (5 min interval × ~72 hours). The only workaround was manually editing ~/.claude/daemon/roster.json to set "workers": {} and ~/.claude/jobs/<id>/state.json to "state": "completed". This is a financial safety issue — users have no reasonable way to know the loop is still running after Ctrl+C.
5 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is one of the most dangerous failure modes in the current Claude Code agent model: a session that you believe you've stopped continues executing and billing against your account with no way to observe or kill it from the CLI.
The root cause you've described —
ScheduleWakeupnot being cancelled on Ctrl+C — is a lifecycle ownership problem. The interrupt signal reaches the foreground session but the scheduled wakeup is registered with the daemon, which has its own lifecycle. Until the daemon exposes a cancellation API (or the CLI writes a "cancelled" tombstone the daemon respects), pressing Ctrl+C is effectively advisory.A few things that could help in the interim:
claude session list(if available in your version) should show running/scheduled sessions — killing the session ID there stops the daemon's respawn loop.pkill -f 'claude'kills the daemon process itself. Extreme, but it stops runaway billing.~/.claude/— deleting the pending wakeup entry directly (if you can identify the file) should prevent respawn without killing the daemon.The broader point is valid: any agent system that can self-schedule must expose a cancellation surface that survives Ctrl+C. This is a hard problem that goes beyond a single hook — it requires the agent runtime to treat "user wants to stop" as an invariant that overrides all pending state.
I'm building Claudeverse (claudeverse.ai), a scheduling and lifecycle coordination layer for Claude Code agents. Unattended runaway cost and uninterruptible daemon loops are core failure modes we're designing around. This issue is a good articulation of why that layer needs to exist outside the CLI itself.
Adding a second reproduction of what looks like the same root cause, reached through a different path: an orphaned interactive session with session-scoped scheduled tasks (CronCreate), no /loop involved.
Environment: macOS (Darwin 25.5.0), Claude Code 2.1.156 and 2.1.161 (daemon upgraded mid-incident)
Timeline
--fork-session --resume). The terminal was later closed without exiting the session.[bg] bg adopt: adopted=1 respawned=0 dead=0at each daemon restart.| Kill attempt | Result |
|---|---|
| SIGTERM on the original orphaned process | Died, then the daemon respawned the session ~40s later under a new pid running
--resume <session-id>, registered askind: bgwith ajobIdand the auto-generated name "Backup assistance requested" || SIGTERM on the respawned bg session | Signal ignored. Process kept running |
| SIGKILL on the session and its pty-host | Both died, respawned again within ~60s, new pids |
|
rm ~/.claude/sessions/<pid>.jsonthen SIGKILL | Respawned again. That file is a record of the registration, not the driver |~/.claude/daemon.logwhen a supervised worker dies and gets respawned.The persistent driver is the
workersmap in~/.claude/daemon/roster.json. As long as the entry exists, the supervisor treats the session as a job that must stay alive.Working workaround
This terminates supervised background sessions, clears the roster, and the daemon restarts on demand with an empty roster. Note it is global: it kills every supervised background session across all projects, not just the affected one. We could not find a per-session stop that survives the respawn behaviour (
claude agentsis TUI-only, with no scriptable kill subcommand).Two documentation gaps this exposed
Can provide full process tables, roster snapshots, and daemon log excerpts if useful for triage.
This feels like a lifecycle bug more than a scheduling bug. Once a wakeup is registered with a daemon, Ctrl+C has to write a durable "stop wins" marker that the daemon checks before it does anything else, otherwise the foreground session dies but the billing loop survives.
The practical fix is probably three pieces together: a cancellation tombstone keyed to the session/job id, a real
list/killsurface that reads the same daemon registry the supervisor uses, and a receipt for every wakeup / cancel decision so people can explain the bill after the fact. Without that, Ctrl+C is basically advisory.We hit the same class of failure building around coding agents, which is a big reason we just launched MartinLoop on Product Hunt yesterday. Even if Anthropic solves this natively, I think the broader lesson sticks: agents need a stop condition that survives the terminal.
Keeping this open — confirmed, and distinct from the linked issues. What's described here is two compounding lifecycle bugs:
exit 143 → attempt 2observation) as a crash and restarts the worker. This needs its own fix — a user-initiated stop should leave a marker the supervisor respects, and there should be a CLI way to list and stop background jobs. Yourstate.json/roster.jsonexcerpts were exactly the right diagnostics, thanks.The CronCreate repro without /loop is consistent with this — it's the scheduled-task lifecycle generally, not /loop-specific.
Until the daemon-side fix lands, the reliable stop is ending the daemon process itself rather than the worker (the supervisor is what restarts workers):
claude daemon stop.Given the cost impact reported here, this should stay open and tracked rather than auto-closed as a duplicate.