Auto-backgrounded/interrupted Bash commands outlive their turn and can destructively race later commands
Long-running Bash tool calls that the harness auto-backgrounds (or that get interrupted/superseded mid-turn) keep executing invisibly across many subsequent turns. In my session, several superseded rm -rf <dir> && git clone … attempts completed minutes later and wiped a directory that newer commands had already successfully rebuilt — a destructive race the model couldn't anticipate because nothing surfaced the "dead" commands as still alive. It took several wipe/rebuild cycles to diagnose.
Requests:
- Interrupting/superseding a command should actually terminate it (or clearly mark it as still-running).
- Surface still-pending background commands prominently when a new command targets the same path — completion notifications exist, but nothing warns that a superseded destructive command is still pending.
- Consider serializing auto-backgrounded commands that touch the same filesystem path.
Environment: macOS 26.5 (arm64), Claude Desktop 1.9659.2 with bundled Claude Code; GitHub integration connected.
Related, not duplicate: #61568 covers visibility/duration of intentional run_in_background tasks; this issue is about superseded/interrupted commands not terminating and destructively racing later commands. Fixing either helps the other.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
The three harness-level requests here are the right fix — especially (3), serializing auto-backgrounded commands that touch the same path. Nothing a user configures can make an already-dispatched, superseded command terminate, so this does need the harness-side change you're asking for.
That said, for the specific worst outcome you hit — a destructive command racing in later and wiping a rebuilt directory — there's a complementary user-side stopgap that keeps that class of command from ever being dispatched (and therefore from ever being backgrounded or completing late): a
PreToolUsehook onBashthat intercepts destructive commands before they run and exits non-zero to block them.PreToolUseruns before the tool executes and receives the command on stdin, so a blockedrm -rf …never starts.Minimal example —
~/.claude/settings.json:~/.claude/hooks/block-destructive.sh:Honest caveats (it's a stopgap, not a silver bullet):
rm -rfon those paths, so tune the regex (or gate it behind an env override) for your workflow.I verified the exact mechanism on a current build (PreToolUse
Bashmatcher, command read from.tool_input.commandon stdin,exit 2to block); the regex is just an example to adapt. Sharing in case it helps cap the data-loss blast radius while the backgrounding race itself gets addressed.Thanks @yurukusa — adopted, with two tuning notes and two caveats for anyone else picking this up:
Tuning: I narrowed the pattern to catastrophic targets only (
/,~/$HOME,..,/Users/<u>,/home/<u>,/etc,/var,/usr) and left repo-internal cleanup (rm -rf node_modules,dist/) to flow through normalpermissions.askrules instead — that way the hook never fights routine work, and anALLOW_DESTRUCTIVE=1prefix provides a deliberate escape hatch. Pipe-tested 11 cases (6 block / 5 pass) before wiring.Why it composes with (not duplicates)
permissions.ask: ask-rules give human judgment per command but are mode-dependent; a PreToolUse hook also holds in auto/bypass-permissions modes — so the two layers fail independently. Defense in depth until the harness-level (1)–(3) land.Caveat for adopters #1: if you add this hook mid-session, it won't load until restart — that's #65953 / #62656 (and the file-creation variant was #66765). Restart before trusting it.
Caveat #2 (the original point stands): this prevents dispatch of matching commands; it cannot terminate an already-backgrounded one, and it does nothing for non-
rmdestructive races (a backgroundedgit cloneclobbering a directory is how mine started). The harness-side termination/serialization asks remain the actual fix.This is a temporal authorization bug: the command may have been approved at dispatch time, but its authority should not survive cancellation, supersession, or a conflicting rebuild of the same path.
The harness-level fix should terminate the whole process group, not only the parent shell, and attach a lease to the resolved target set. A newer command touching an overlapping path should either wait or revoke the older lease. Completion events also need a dispatch id so a late result cannot be mistaken for the current turn.
Disclosure: I maintain gate.cat. A pre-execution veto can stop a destructive command before dispatch, but it cannot make an already-running process obey later conversational state; cancellation and path serialization have to live in the harness.