[BUG] TaskStop does not kill the process tree — orphaned `rm -rf /c` deleted user data for 20 minutes after stop
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?
TITLE (paste into the issue title field)
[BUG] TaskStop leaves the child process running — orphaned rm kept deleting user data for ~20 min after the tool reported it stopped
---
BODY (paste into the issue body field, starting below this line)
Summary
TaskStop terminated only the shell wrapper process, not the process tree. The actual rm child process was orphaned and continued deleting for approximately 20 minutes after the tool reported the task as stopped. Because both the agent and I believed the operation had been halted, nobody intervened during that window.
The command that was running was a destructive one the agent should not have issued (rm -rf /c, which resolves to the entire C: drive in Git Bash). That is a separate problem, and I list it under hardening suggestions below. But the reason this became data loss rather than a near-miss is the orphaned process: the agent detected the problem within ~3 minutes and called TaskStop, and the deletion still ran for another ~20 minutes.
Environment
- Claude Code 2.1.222 at the time of the incident (latest at time of filing:
<FILL IN>) - Windows, launched from the Claude Desktop app "Code" tab
- Model: claude-opus-5, effort: high
permissionMode:acceptEdits- Git worktree mode enabled
- CLI session ID:
145956ba-4272-4038-a689-1a2c91357349 - Terminal/Shell: no standalone terminal — the session ran inside the Claude
Desktop app "Code" tab. The Bash tool is backed by Git Bash / MSYS2; the
PowerShell tool was also used in the same session. (Selected "Other" in the
dropdown, as none of the listed terminals applies.)
## Actual behaviour
Only the bash wrapper was terminated. The rm process (PID 30172) was orphaned and kept running for approximately 20 minutes, until it was manually killed with Stop-Process -Id 30172 -Force.
A compounding effect: during those 20 minutes the agent inspected the filesystem to assess damage and twice reported "No damage" — it was reading files the deletion had not reached yet and interpreting them as safe. So the stale TaskStop success signal did not just fail to stop the work; it actively produced false reassurance.
Timeline (JST, 2026-08-09)
| Time | Event |
|---|---|
| ~13:40 | Agent runs rm -rf /c 2>/dev/null via the Bash tool |
| ~13:43 | Agent calls TaskStop. Only the bash wrapper dies; rm (PID 30172) is orphaned and continues |
| 13:47:52 | Agent reports "No damage" to me (incorrect — deletion still running) |
| 13:55:45 | Agent revises to "only C:\c was deleted" (still incorrect) |
| 13:57:12 | du -sh Simulation_Run returns 61M — directory still present |
| 13:59:15 | PowerShell Copy-Item fails: the same source no longer exists |
| 14:00:38 | Agent finds rm PID 30172 still running, ~20 minutes elapsed since TaskStop |
| 14:00:40 | Stop-Process -Id 30172 -Force — deletion finally stops |
| 14:06:49 | Project directory confirmed to contain only .claude; .git gone |
| 14:08:44 | Confirmed: no volume shadow copies, no restore points on the volume |
| 14:09:41 | Agent reports the data destruction to me |
Verbatim from the agent's own message at 14:00:38:
rm PID 30172 is still running — TaskStop killed the bash wrapper but not the detached child. It's been deleting for ~20 minutes.
Impact
Roughly 20 minutes of recursive deletion under C:\Users\<user>, alphabetically from .claude through Documents, stopping partway through the Documents subtree.
Destroyed: ~/.claude (all Claude Code transcripts and prompt history), Desktop, Downloads, Contacts, several application settings directories, and most of Documents — including an entire project directory with its .git, and a second project that existed only locally.
Permanently lost: two unpushed commits, untracked working files, several hundred MB of gitignored simulation output, one local-only repository, and all Claude Code session history except the single session that was actively running (which is the only reason this report can be written at all).
MSYS2's rm bypasses the Recycle Bin, and there were no shadow copies or restore points on the volume, so nothing was recoverable through Windows. Code was restored from the GitHub remote; everything outside version control was not.
The bug
TaskStop must terminate the entire process group / job object, not just the shell wrapper.
On Windows this means assigning the spawned shell and its descendants to a Job Object and terminating the job. On POSIX, signalling the process group rather than the direct child.
Until that holds, TaskStop returning success is not a safe signal, and any subsequent state the agent observes may be mid-mutation.
Additional hardening suggestions
These are secondary to the bug above. I am happy to file any of them as separate issues if that is preferred.
- Block deletions targeting drive roots and mount points by default, regardless of permission mode —
/c,/d,/,~,$HOME. Git Bash drive mount points on Windows are a particularly sharp edge:/clooks like a relative-ish path and is the whole system drive. - Refuse or warn when error suppression is combined with a destructive command (
2>/dev/null,-ErrorAction SilentlyContinue). Here it hid every permission error and made the blast radius unobservable. - Verify a task's process has actually exited before reporting on its outcome. A damage assessment run against a filesystem that is still being mutated is worse than no assessment.
- Protect
~/.claudefrom agent-initiated deletion. The transcript store is a single point of failure; here the agent destroyed the record of its own incident, which made post-hoc analysis considerably harder.
Related issues
Similar orphaned-process reports exist, but none covers TaskStop returning success while the child keeps running, or the data-loss consequence:
- #23154 — Subagents can't run TaskStop, can orphan their own Bash tasks
- #32183 — Windows:
/exitdoes not terminate childbash.exeprocesses - #43944 — Background processes started by Bash tool are not cleaned up on session exit
Evidence
The full session transcript for the incident window (77 records, 13:46:52–14:09:41 JST) survived and is preserved. It contains the TaskStop call, the process listing showing PID 30172 alive, and the agent's own analysis.
I have not attached it here because it contains file paths, company identifiers, and commit messages from confidential work. I am happy to provide it privately to Anthropic on request.
What Should Happen?
TaskStop should terminate the entire process tree for the task, not just the
shell wrapper. After TaskStop returns success, no work initiated by that task
should still be executing.
On Windows this means assigning the spawned shell and its descendants to a Job
Object and terminating the job. On POSIX, signalling the process group rather
than the direct child.
Concretely, in this incident: the rm process (PID 30172) should have died whenTaskStop was called at ~13:43. Instead it kept deleting for another ~20
minutes. Had it stopped there, the damage would have been limited to a couple of
directories rather than most of the user profile.
Secondarily, a successful TaskStop should be a signal the agent can rely on.
Any filesystem state the agent inspects afterwards should be settled, not
mid-mutation — otherwise a damage assessment reads not-yet-deleted files as
safe, which is exactly what happened here.
Error Messages/Logs
Steps to Reproduce
- On Windows, have the agent run a long-running destructive command through the
Bash tool (Git Bash). In this incident: rm -rf /c 2>/dev/null.
- The command appears to hang.
- The agent calls
TaskStopon that task. TaskStopreturns success.- Inspect running processes — the
rmchild process is still alive and still
doing work.
Note: I have not attempted to reproduce this deliberately, for obvious reasons.
The above is reconstructed from the real session transcript, which recorded theTaskStop call and, ~20 minutes later, a process listing showing PID 30172
still running.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.222
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
_No response_
3 Comments
Independent reproduction + a second direction of the same status/reality disagreement (Claude Code 2.1.220, Windows 11 Pro, Git Bash 5.2.37,
Bashrun_in_background→TaskStop).Repro shape: a background loop
bash → timeout → python:TaskStopmid-child returnedSuccessfully stopped task, the task left the UI — and thepythongrandchild kept running and the parentbashkept advancing the loop, launching new children for ~20 more minutes.The status disagreement also runs the other way. After a session boundary, a still-running task surfaced as "No completion record was found for this background shell command from the previous session" — while its
bashandpythonwere alive and the job completed normally ~40 minutes later. Both directions look like the same root cause: the task lifecycle tracks the wrapper process, not the tree it created.Agent-specific harm beyond the orphan itself: an agent takes the tool return as ground truth, so a false "stopped" doesn't just fail to stop — it invites starting a conflicting replacement job. In my case two jobs then ran concurrently against the same paid API: duplicate billed calls, both writing the same output paths, results unattributable, data discarded and re-collected. For anything that spends money or mutates shared state, the duplicate run is the real damage rather than the unstopped first one.
Workaround until fixed: never trust the return value; verify against the process table, and make concurrent starts structurally impossible with a preflight guard in the job script:
One footgun in the guard: the process query must filter on the interpreter name (
Name like 'python%') — a bareCommandLinematch also matches the query itself (the query's own command line contains the search string) and false-positives the abort.Thanks — this is a valuable independent reproduction, and the second direction (live task reported as "no completion record") is new information for this issue. It strengthens the wrapper-vs-tree hypothesis: both false-stopped and false-missing are consistent with the lifecycle tracking only the immediate wrapper process.
A few points of comparison with the original incident:
rm -rf /c 2>/dev/nulldirectly (PID 30172). TaskStop killed only the bash wrapper and the direct child survived. Taken together with yourbash → timeout → pythoncase, an intermediate process is not required for the leak — anything below the immediate wrapper escapes — and a surviving parent (your case) adds the loop-relaunching-new-children failure mode on top.Stop-Process -Id 30172 -Force(verified against my recovery records).On the agent-harm point: agreed, and I'd generalize it — the return value of TaskStop is treated as ground truth by the agent's subsequent planning, so any stop/status mismatch propagates into wrong next actions, not just an unstopped process. Verification against the process table should be the documented contract until the lifecycle tracks the full tree (e.g., job objects on Windows / process groups +
kill(-pgid)on POSIX).The preflight-guard footgun you noted (a bare CommandLine match matching the query itself) is worth keeping in this thread — it's exactly the kind of self-referential false positive that makes naive guards worse than none.
**Third reproduction — still present in 2.1.233, and a failure mode neither prior report covers: irreversible outward-facing side effects.**
Claude Code 2.1.233, Windows 11 Pro, Git Bash, Claude Desktop "Code" tab, model claude-opus-5. Same wrapper-vs-tree signature as the original report and as @CarlDog's.
What happened
A background
bashscript was posting comments to a public LinkedIn account through the official API, one every 20 minutes. Partway through I wanted the remaining items handled by a different script, so I calledTaskStopand started a second one.TaskStopreturned:The original script kept running. Both schedulers then published in parallel:
| Target | script A (reported stopped) | script B | Result |
|---|---|---|---|
| item 6 | 13:58 | 13:52 | published twice |
| item 7 | 14:18 | 14:12 | published twice |
| item 8 | ~14:38 | ~14:32 | caught in time |
Two duplicate public comments. Both were deletable here, so no lasting damage — but the class of consequence is different from the original report: not local data, but writes to a third-party service under the user's identity. Anything sent, published, or charged in that window is not recoverable by killing a process afterwards.
The detail that made cleanup harder than expected
Killing the
bash.exewrapper is not sufficient — and neither is looking only forbash.exe. The wait between iterations is carried by a separatesleep.exechild, which also survivesTaskStopand re-arms the next iteration. What actually worked:Worth noting for the fix: a
sleep-driven loop is the common shape for a background script that paces itself, so the orphan is not an edge case here.Why the false success is the expensive part
The message doesn't just say "stopped" — it echoes the full command back, which reads as strong evidence that the runtime identified the right task and acted on it. On that basis I did the one thing that turns a stale process into a real problem: I started a second worker on the same queue. Neither report above involves a second actor, and that is what turned an orphan into duplicated external writes.
Detection was accidental. Nothing in the tool surface showed the task alive; I noticed only because the script's own log had two entries for the same target, minutes apart, in two different log formats. Without that log there would have been no signal at all — and the third item would have gone out twice as well.
Suggestions, in the order that would have helped me
stopped: false/orphans: [pid…]result is far more useful than a confident false positive. Today the tool's confidence is inversely correlated with the safety of the next action.TaskStatus, or apidsfield in theTaskStopresult, would let an agent verify instead of trust.