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
claude --bg never returns to a caller that captures its stdout through a pipe — the job finishes, the process exits, and the caller is still waiting.
Measured in spike #330, from an agent tool call. Claude Code 2.1.226.
What was measured
Same command, two ways of taking its output:
how stdout was taken
result
pipe (… | cat, or captured by a harness)
never returned at 110 s — and by then the process had already exited and the job was already done
file (… > out.txt)
exit 0 in 0.87 s
The cause: claude --bg hands the job to a detached supervisor daemon, and the daemon inherits the caller's file descriptors. A pipe stays open as long as any process holds the write end, so the caller waits on a pipe that a long-lived daemon is keeping alive — not on the job, which finished.
Why this is filed rather than shrugged at
It is #184's exact shape, and #184 cost this repo a shipped feature. That bug was a printf emitting a correct escape sequence whose stdout the harness captured: it worked from a shell the operator typed into, was silent from an agent's tool call, and reported success either way. push.sh exists in its current form because of it, and its header carries the measurement of what an agent's position actually is — tty is ??, session 0, /dev/tty unopenable, [ -t 1 ] false.
An agent's tool call captures stdout by construction. So this is not an edge case for helm — it is the default case for every agent that would ever run claude --bg, and the failure is a hang rather than an error, which is the worst available shape: indistinguishable from a slow spawn, and it burns the caller's turn.
This repo has now hit the same class three times — #124, #184, and today's canvas gate accepting exit 0 as delivery. This is the fourth, in a primitive nobody has built on yet, which is the cheapest moment to know about it.
What to do
Nothing in helm calls claude --bg today, so this is a rule to establish before anything does, not a regression to fix:
Redirect stdout to a file, never a pipe, in any helm code path or documented snippet that spawns a background agent. The job's real output is read afterwards through claude agents --json, ~/.claude/jobs/<id>/state.json or claude logs <id> — all of which the spike confirmed work TTY-free.
If a skill or AGENTS.md ever documents claude --bg, the redirect is part of the documented invocation, with the reason attached — a documented snippet is an implementation, and this one hangs.
The rule is written down where someone about to spawn a background agent will see it.
Any helm code that spawns one uses a file, and something proves it — a hang has no error to assert on, so the test is that the call returns, with a timeout that fails rather than waits.
Measured from an agent tool call, which is the only seat where this happens.
Related: #330 (the spike), #184 and #124 (the same class), #282 (loud refusal over silent success).
claude --bgnever returns to a caller that captures its stdout through a pipe — the job finishes, the process exits, and the caller is still waiting.Measured in spike #330, from an agent tool call. Claude Code 2.1.226.
What was measured
Same command, two ways of taking its output:
… | cat, or captured by a harness)… > out.txt)The cause:
claude --bghands the job to a detached supervisor daemon, and the daemon inherits the caller's file descriptors. A pipe stays open as long as any process holds the write end, so the caller waits on a pipe that a long-lived daemon is keeping alive — not on the job, which finished.Why this is filed rather than shrugged at
It is #184's exact shape, and #184 cost this repo a shipped feature. That bug was a
printfemitting a correct escape sequence whose stdout the harness captured: it worked from a shell the operator typed into, was silent from an agent's tool call, and reported success either way.push.shexists in its current form because of it, and its header carries the measurement of what an agent's position actually is —ttyis??, session0,/dev/ttyunopenable,[ -t 1 ]false.An agent's tool call captures stdout by construction. So this is not an edge case for helm — it is the default case for every agent that would ever run
claude --bg, and the failure is a hang rather than an error, which is the worst available shape: indistinguishable from a slow spawn, and it burns the caller's turn.This repo has now hit the same class three times — #124, #184, and today's canvas gate accepting exit 0 as delivery. This is the fourth, in a primitive nobody has built on yet, which is the cheapest moment to know about it.
What to do
Nothing in helm calls
claude --bgtoday, so this is a rule to establish before anything does, not a regression to fix:claude agents --json,~/.claude/jobs/<id>/state.jsonorclaude logs <id>— all of which the spike confirmed work TTY-free.AGENTS.mdever documentsclaude --bg, the redirect is part of the documented invocation, with the reason attached — a documented snippet is an implementation, and this one hangs.push.shrefuses when it cannot find a pty helm owns. A loud refusal beats a hang, and that is the lesson push.sh exits 0 from outside a helm pane, having written its OSC into some other terminal #282 just paid for.Acceptance
Related: #330 (the spike), #184 and #124 (the same class), #282 (loud refusal over silent success).