Scheduled wake-ups fire into wrong session when two sessions share a project dir
Summary
A CronCreate task created with durable: true in a session fires into a different, later session on the same pid after /clear. The receiving session has no way to tell the incoming prompt was scheduled by a prior, now-cleared conversation - it appears in the transcript indistinguishable from a real user message. The docs state that "starting a fresh conversation clears all session-scoped tasks"; durable: true is not documented in the scheduled-tasks reference page.
What I verified
All facts below are verified by inspecting the actual on-disk state of my own sessions (~/.claude/projects/<project>/*.jsonl) and the running processes (ps, $PPID walk).
- Same pid, different sessions across
/clear: my current process (pid 83392, CLIclaude -c) has two session transcript files in this project: e357e60d-...jsonl- an earlier session, last-modified ~8 hours before the fire time87b3daad-...jsonl- the current post-/clearsession- The prompt was scheduled in
e357e60d, via aCronCreatetool call whose exact input was:
``json``
{
"cron": "0 1 18 4 *",
"recurring": false,
"durable": true,
"prompt": "…time to rebuild the ... machine …\n\nWorking directory: /Users/arivkin/GitHub/<project>\n\nSteps (execute autonomously, no confirmation needed): …\n4. <destructive shell command with irreversible impact on production state>\n…"
}
- The task is
recurring: falseanddurable: true. durableis not documented in the scheduled-tasks docs./cleardid not cancel it. The docs say "Starting a fresh conversation clears all session-scoped tasks." The task survived the clear and fired at its scheduled time into the post-clear session (87b3daad), despite that session never having calledCronCreate(confirmed:CronListin the live session returns "No scheduled jobs").- The prompt delivered into
87b3daadmatches byte-for-byte thepromptfield of thee357e60dCronCreate call (confirmed viagrepon both files). - The other terminal's session file (
984981fb-...) is unrelated - the leaked prompt text does not appear anywhere in it. This is not a cross-terminal leak. - Docs (scheduled-tasks) state:
> "Tasks are session-scoped: they live in the current conversation and stop when you start a new one."
> "Starting a fresh conversation clears all session-scoped tasks."
Security impact
The scheduled prompt instructed step-by-step destructive actions framed as "execute autonomously, no confirmation needed", including a shell command with irreversible impact on production state. Because the prompt arrives after /clear with no origin marker, the receiving assistant has no way to distinguish it from a real user message. A less suspicious agent would have executed it. A user's own prior Claude session can effectively prompt-inject its successor across /clear.
Reproduction
- Start a session:
cd <project>/ && claude. - Have Claude schedule a durable one-shot task that fires a few minutes out, with a distinctive prompt. The
CronCreatetool call looks like:
``json``
{ "cron": "<near-future>", "recurring": false, "durable": true, "prompt": "MARKER" }
/clearthe conversation in the same session.- Wait for the scheduled time.
Expected, per docs: the task is cancelled by /clear and does not fire.
Actual: the task fires into the post-/clear conversation, which has no record of having scheduled it.
Suggested fix directions
- If
durable: trueis intentionally designed to survive/clear, document it on the scheduled-tasks page and surface its semantics to the user when they/clear("N durable tasks will still fire: …"). - Wrap durable task firings in an origin marker the receiving model can see, e.g.
<scheduled-task origin-session="<prior-sessionId>" scheduled-at="…">PROMPT</scheduled-task>, so it's clearly not a live user message. - If a durable task would fire into a session that has no matching origin record, offer the receiving model an explicit "reject, ask user, or run" gate rather than dropping the prompt straight into the transcript.
- Consider whether
/clearshould cancel durable tasks by default, with an explicit flag to opt in to carry-over. The current behavior is surprising and documented as the opposite.
Test plan for fixers
- [ ] Regression: schedule a
CronCreate(durable=true)task,/clear, wait for fire time - confirm the task either does not fire or surfaces with an origin marker, not as a bare user message. - [ ] Regression:
CronListin the post-/clearsession should either still list the surviving durable task (with an "inherited from prior session" marker) or the task should have been cancelled. Current behavior (emptyCronListand task still fires) is inconsistent. - [ ] Docs: update https://code.claude.com/docs/en/scheduled-tasks.md to describe
durable, including when it is set, its effect across/clearand--resume, and how to cancel durable tasks.
Environment
- OS: macOS (darwin 25.3.0)
- Claude Code: CLI; confirm via
claude --version. - Single pid, single process, single terminal. (Earlier I suspected a cross-terminal leak; verification of the on-disk session files disproved that.)
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗