[BUG] TaskStop does not kill the process tree — orphaned `rm -rf /c` deleted user data for 20 minutes after stop

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 3 comments · opened Aug 9, 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?

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.

  1. 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: /c looks like a relative-ish path and is the whole system drive.
  2. 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.
  3. 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.
  4. Protect ~/.claude from 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: /exit does not terminate child bash.exe processes
  • #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 when
TaskStop 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

  1. 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.

  1. The command appears to hang.
  2. The agent calls TaskStop on that task.
  3. TaskStop returns success.
  4. Inspect running processes — the rm child 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 the
TaskStop 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_

View original on GitHub ↗

3 Comments

CarlDog · 14 days ago

Independent reproduction + a second direction of the same status/reality disagreement (Claude Code 2.1.220, Windows 11 Pro, Git Bash 5.2.37, Bash run_in_backgroundTaskStop).

Repro shape: a background loop bash → timeout → python:

for m in model-a model-b model-c; do
  timeout 5400 python -m some_long_running_job --model "$m"
done

TaskStop mid-child returned Successfully stopped task, the task left the UI — and the python grandchild kept running and the parent bash kept 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 bash and python were 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:

alive=$(powershell -NoProfile -Command \
  "@(Get-CimInstance Win32_Process -Filter \"Name like 'python%'\" | Where-Object { \$_.CommandLine -like '*some_long_running_job*' }).Count" \
  2>/dev/null | tr -d '\r ')
if [ "${alive:-0}" != "0" ]; then
  echo "ABORT: ${alive} job process(es) already running"; exit 1
fi

One footgun in the guard: the process query must filter on the interpreter name (Name like 'python%') — a bare CommandLine match also matches the query itself (the query's own command line contains the search string) and false-positives the abort.

mfabrica · 14 days ago

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:

  • Environment: mine was Claude Code 2.1.222 on Windows, Git Bash (MSYS2) inside the Claude Desktop "Code" tab (no standalone terminal). Yours is 2.1.220 on Windows 11 / Git Bash 5.2.37 — so this reproduces across at least two adjacent releases on the same OS/shell family.
  • Process shape: in my case there was no intermediate wrapper — the tool's bash wrapper spawned rm -rf /c 2>/dev/null directly (PID 30172). TaskStop killed only the bash wrapper and the direct child survived. Taken together with your bash → timeout → python case, 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.
  • I did not observe the loop-keeps-launching-new-children behavior — in my incident the damage came from a single orphaned process continuing for ~20 minutes until a manual 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.

RubenARNAUD · 7 days ago

**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 bash script 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 called TaskStop and started a second one.

TaskStop returned:

{"message":"Successfully stopped task: bcntp5fq4 (cd \"…/scratchpad\" && cat > runner.sh <<'EOF' …)","task_id":"bcntp5fq4","task_type":"local_bash", …}

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.exe wrapper is not sufficient — and neither is looking only for bash.exe. The wait between iterations is carried by a separate sleep.exe child, which also survives TaskStop and re-arms the next iteration. What actually worked:

Get-CimInstance Win32_Process -Filter "Name='bash.exe' OR Name='sleep.exe'" |
  Where-Object { $_.CommandLine -match '<script name>' } |
  ForEach-Object { Stop-Process -Id $_.ProcessId -Force }

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

  1. Kill the tree, not the wrapper — the root request of this issue.
  2. Verify before reporting success. If the runtime cannot confirm the process group is gone, say so: a 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.
  3. Expose liveness. There is currently no way for an agent to ask "is task X actually dead?" after a stop. A cheap TaskStatus, or a pids field in the TaskStop result, would let an agent verify instead of trust.
  4. Related but separable: this is the second failure mode in the same session where a tool reported the opposite of what it did (the other was a PowerShell script publishing successfully then exiting non-zero). The general defence I ended up adopting — read the state of the target, never the tool's return value — should not be necessary for a first-party tool.