[BUG] ScheduleWakeup persists after Ctrl+C, daemon auto-respawns loop unattended causing unbounded token spend

Status Open
Reported on v2.1.160
Maintainer reply None cached
Activity 5 comments · opened Jun 2, 2026

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

  1. Run any skill via /loop with a short interval (e.g. /loop 5m /my-skill)
  2. Press Ctrl+C to stop — you expect the loop to be cancelled
  3. Close the terminal window
  4. Check ~/.claude/daemon/roster.json — the worker entry persists
  5. 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.

View original on GitHub ↗

5 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/57660
  2. https://github.com/anthropics/claude-code/issues/61735
  3. https://github.com/anthropics/claude-code/issues/58662

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

kcarriedo · 2 months ago

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 — ScheduleWakeup not 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:

  1. claude session list (if available in your version) should show running/scheduled sessions — killing the session ID there stops the daemon's respawn loop.
  2. If that's not available: pkill -f 'claude' kills the daemon process itself. Extreme, but it stops runaway billing.
  3. The daemon's session state files live under ~/.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.

markneville · 2 months ago

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

  1. Friday evening: an interactive session was resumed in a terminal (--fork-session --resume). The terminal was later closed without exiting the session.
  2. The session survived 4 days. Its session-scoped scheduled tasks kept firing the whole time, duplicating the scheduled work of newer sessions in the same project.
  3. The daemon adopted the orphan into supervision. Its log shows [bg] bg adopt: adopted=1 respawned=0 dead=0 at each daemon restart.
  4. Once adopted, the session could not be killed:

| 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 as kind: bg with a jobId and 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>.json then SIGKILL | Respawned again. That file is a record of the registration, not the driver |

  1. Parent chain of the respawned process shows the daemon is the respawner:
respawned session (pid 76866)
  parent: bg-pty-host (pid 76863)
    parent: claude daemon run (pid 61560)
  1. The respawns are silent. Nothing is written to ~/.claude/daemon.log when a supervised worker dies and gets respawned.

The persistent driver is the workers map 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

claude daemon stop --any
# -> "stopped (terminated 1 background session)"

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 agents is TUI-only, with no scriptable kill subcommand).

Two documentation gaps this exposed

  • The scheduled-tasks docs say closing the terminal stops tasks from firing. When the daemon adopts the orphaned session instead, they keep firing indefinitely.
  • The auto-respawn-on-death behaviour for supervised sessions is not documented, and neither is the "Backup assistance requested" job name.

Can provide full process tables, roster snapshots, and daemon log excerpts if useful for triage.

Keesan12 · 2 months ago

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/kill surface 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.

bhosmer-ant · 2 months ago

Keeping this open — confirmed, and distinct from the linked issues. What's described here is two compounding lifecycle bugs:

  1. Interrupt doesn't cancel pending scheduled wakeups. A fix merged this week (in an upcoming release) makes Esc/Ctrl+C cancel a pending loop wakeup in the foreground session; a follow-up adding visible feedback so you can tell it worked is in review. That improves part of this report, but only the foreground path.
  1. The daemon respawns a killed worker. This is the part the above does not cover, and it's why the loop survived a closed terminal: an armed wakeup keeps the background worker from retiring, and the supervisor treats SIGTERM (your exit 143 → attempt 2 observation) 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. Your state.json/roster.json excerpts 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.