You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
v2: shell: no way to stop a running background command #36424
The handle-at-launch half is fixed: since v2.0.0 (#45461), background shell responses include the shell ID and output file at launch. Still missing: a model-callable way to stop a running background command using that ID.
Current state (v2 >= 2.0.0)
Launch:Command moved to the background (shell ID: sh_...). + Output is streaming to: <file> (shellID in metadata).
Stop: human-only — TUI Ctrl+D calls the shell.remove client API; no shell parameter or dedicated tool lets the model stop a background command.
This issue might be a duplicate of existing issues. Please check:
V2: define shell backgrounding semantics #34366: Tracks the design and implementation of V2 shell backgrounding semantics, including cancellation, process handles, and owner/session-bound observation — which is the same gap you're describing here.
Thanks for tracking this — I hit exactly this on Windows (dev servers that can't be stopped without pkill/taskkill by name), and I'd like to propose a concrete shape that also addresses #34366.
Prior art
Claude Code backgrounds a running Bash call (Ctrl+B), returns a task ID, writes output to a file the agent reads later, and auto-terminates on session exit or >5 GB output (disable via CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1).
Cline executes commands directly in the terminal, watches output in real time, and reacts to new output as it appears (with cancellation via the SDK's abort signal).
Both prove the model: the agent launches, gets a handle, observes, and kills — instead of blocking until exit or the 60s timeout.
Proposed API surface
Three pieces, all session-scoped (jobs are not cross-session addressable, mirroring Claude Code):
bash gains async?: boolean. When set, the tool returns immediately with { job_id, pid, started_at } instead of blocking. Output is captured into a 1 MiB in-memory ring buffer with file spill (reusing the existing truncation path). The synchronous path is unchanged when async is omitted.
bash_status({ job_id, tail_bytes? }) → { state: "running"|"exited"|"killed", elapsed_ms, exit_code?, output_so_far }. Non-blocking poll. The agent calls it in a loop, reasoning between polls (wait / kill / proceed). Suggested throttling: min 500 ms, max 5 s, backoff when output is unchanged across two polls.
V2: define shell backgrounding semantics #34366: answers the open questions — background?: boolean (I'd call it async to avoid overloading the subagent background term), return shape, completion delivery via bash_status, owner-bound get/cancel, output retention (ring + spill), and platform cleanup guarantees.
Smallest viable first PR
Ship async:true + bash_status first; bash_kill as a follow-up. Before bash_kill lands, the agent can still kill via a normal synchronous bash call using the pid returned by the async launch (taskkill /PID <pid> /T /F on Windows). This keeps PR 1 focused on the observation problem (the harder half) and lets the registry/ring design bake before committing kill semantics.
I've verified the current shell.ts on dev — the spawner.spawn / Stream.decodeText(handle.all) / handle.kill({ forceKillAfter }) primitives are exactly what async mode needs, so the change is a fork inside run() plus a new JobRegistry, not a rewrite.
Happy to open a PR against dev if there's interest. If BackgroundJob (referenced in #34366 as already existing for subagent background mode) is the intended shared primitive, I'm glad to build on top of it instead of a standalone registry — just point me at its interface.
changed the title [-]v2: shell: no way to stop or cancel a running background command[/-][+]v2: shell: no handle at launch to stop/cancel running background commands[/+]on Jul 23, 2026
Updating with new v2 findings: background shell commands do return a call ID (call_00_GHYwLiUePO5p7d23ATlI7431) — but only in the completion notification, not at launch. This confirms the infrastructure tracks them; the fix would be to expose the ID at launch time, which would then enable a cancel/kill mechanism. Keeping this open to avoid stale.
changed the title [-]v2: shell: no handle at launch to stop/cancel running background commands[/-][+]v2: shell: no way to stop a running background command[/+]on Sep 14, 2026
Updated the issue to reflect the current state: the launch gap is fixed — since v2.0.0 (#45461), background shell responses include the shell ID and output file. The open gap is now just model-facing cancellation (human-only via TUI Ctrl+D today). #48352 adds shell_stop and closes this issue; #46721 adds typed stop outcomes.
Still valid on v2.0.23 (v2 tip fad352c): the shell tool still exposes only command/workdir/timeout/background — no stop/cancel parameter and no shell_stop tool. Stop remains human-only via shell.remove (TUI Ctrl+D). PR #48352 is still unmerged.
Thanks for the clear write-up! You're asking for a model-callable way to stop a running background shell command, using the shellID it gets back at launch, so agents can clean up servers or watchers they started without asking the user to press Ctrl+D.
I checked the current code: the shell tool has no stop parameter and there's no separate stop tool, so stopping is still only possible from the client side (shell.remove). I'm marking this as a feature request for the maintainers to consider.
Activity
github-actions commentedon Jul 11, 2026
This issue might be a duplicate of existing issues. Please check:
Klark61 commentedon Jul 13, 2026
Thanks for tracking this — I hit exactly this on Windows (dev servers that can't be stopped without
pkill/taskkillby name), and I'd like to propose a concrete shape that also addresses #34366.Prior art
Ctrl+B), returns a task ID, writes output to a file the agent reads later, and auto-terminates on session exit or >5 GB output (disable viaCLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1).Both prove the model: the agent launches, gets a handle, observes, and kills — instead of blocking until exit or the 60s timeout.
Proposed API surface
Three pieces, all session-scoped (jobs are not cross-session addressable, mirroring Claude Code):
bashgainsasync?: boolean. When set, the tool returns immediately with{ job_id, pid, started_at }instead of blocking. Output is captured into a 1 MiB in-memory ring buffer with file spill (reusing the existing truncation path). The synchronous path is unchanged whenasyncis omitted.bash_status({ job_id, tail_bytes? })→{ state: "running"|"exited"|"killed", elapsed_ms, exit_code?, output_so_far }. Non-blocking poll. The agent calls it in a loop, reasoning between polls (wait / kill / proceed). Suggested throttling: min 500 ms, max 5 s, backoff when output is unchanged across two polls.bash_kill({ job_id, signal? })→{ killed, pid }.taskkill /PID <pid> /T /F— the/Tis load-bearing. Dev servers (vite,next dev) spawn worker children that hold the stdout pipe open; killing only the parent leaves orphans that keep the pipe alive (the Bash tool hangs until timeout on Windows when spawned process keeps a stdout/stderr pipe open (dev servers, daemons) #32504 symptom)./Twalks the tree,/Fforces.SIGTERM, escalating toSIGKILLafter 5 s.A session-scope finalizer kills any still-running jobs when the session ends, so an agent that forgets to call
bash_killdoesn't leak processes.How this lands on the existing issues
async:truereturns immediately; the agent polls instead of blocking on pipe EOF.(no output)with exit 0 on first call when usingworkdir#35511 ((no output)race): in async mode the reader fiber is owned by the job, not the tool-call scope, so it isn't interrupted on tool return — the ring always has whatever was drained.background?: boolean(I'd call itasyncto avoid overloading the subagentbackgroundterm), return shape, completion delivery viabash_status, owner-bound get/cancel, output retention (ring + spill), and platform cleanup guarantees.Smallest viable first PR
Ship
async:true+bash_statusfirst;bash_killas a follow-up. Beforebash_killlands, the agent can still kill via a normal synchronousbashcall using thepidreturned by the async launch (taskkill /PID <pid> /T /Fon Windows). This keeps PR 1 focused on the observation problem (the harder half) and lets the registry/ring design bake before committing kill semantics.I've verified the current
shell.tsondev— thespawner.spawn/Stream.decodeText(handle.all)/handle.kill({ forceKillAfter })primitives are exactly what async mode needs, so the change is a fork insiderun()plus a newJobRegistry, not a rewrite.Happy to open a PR against
devif there's interest. IfBackgroundJob(referenced in #34366 as already existing for subagent background mode) is the intended shared primitive, I'm glad to build on top of it instead of a standalone registry — just point me at its interface.[-]v2: shell: no way to stop or cancel a running background command[/-][+]v2: shell: no handle at launch to stop/cancel running background commands[/+]Ploppy3 commentedon Jul 23, 2026
Updating with new v2 findings: background shell commands do return a call ID (
call_00_GHYwLiUePO5p7d23ATlI7431) — but only in the completion notification, not at launch. This confirms the infrastructure tracks them; the fix would be to expose the ID at launch time, which would then enable a cancel/kill mechanism. Keeping this open to avoid stale.[-]v2: shell: no handle at launch to stop/cancel running background commands[/-][+]v2: shell: no way to stop a running background command[/+]Ploppy3 commentedon Sep 14, 2026
Updated the issue to reflect the current state: the launch gap is fixed — since v2.0.0 (#45461), background shell responses include the shell ID and output file. The open gap is now just model-facing cancellation (human-only via TUI Ctrl+D today). #48352 adds
shell_stopand closes this issue; #46721 adds typed stop outcomes.Ploppy3 commentedon Oct 5, 2026
Still valid on v2.0.23 (v2 tip
fad352c): theshelltool still exposes onlycommand/workdir/timeout/background— no stop/cancel parameter and noshell_stoptool. Stop remains human-only viashell.remove(TUI Ctrl+D). PR #48352 is still unmerged.opencodejr commentedon Oct 28, 2026
Thanks for the clear write-up! You're asking for a model-callable way to stop a running background shell command, using the
shellIDit gets back at launch, so agents can clean up servers or watchers they started without asking the user to press Ctrl+D.I checked the current code: the shell tool has no stop parameter and there's no separate stop tool, so stopping is still only possible from the client side (
shell.remove). I'm marking this as a feature request for the maintainers to consider.Related:
shell_stoptool for this, but was closed without being merged