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
Formatter hangs indefinitely when spawned command has background child processes (stdout/stderr pipe never closes) #26032
When using a custom formatter (e.g. pants --no-pantsd fix \$FILE) that spawns background/worker child processes, the formatter call in opencode hangs indefinitely and the edit/write tool never returns.
Root Cause
Introduced in commit 5cd54ec34 ("refactor(format): use ChildProcessSpawner instead of Process.spawn", Mar 27 2026).
Before this refactor, the formatter used Process.spawn with explicit stdout: "ignore" and stderr: "ignore" options:
// old code — before 5cd54ec34constproc=Process.spawn(item.command.map((x)=>x.replace("$FILE",filepath)),{cwd: Instance.directory,env: { ...process.env, ...item.environment},stdout: "ignore",// ← pipes not createdstderr: "ignore",// ← pipes not created},)constexit=awaitproc.exited
After the refactor, these options were dropped:
// new code — missing stdout/stderr optionsChildProcess.make(replaced[0]!,replaced.slice(1),{cwd: dir,env: item.environment,extendEnv: true,// no stdout/stderr → defaults to "pipe"})
The CrossSpawnSpawner defaults to stdio: ["pipe", "pipe", "pipe"] when no stdout/stderr options are provided. This creates OS pipes for the formatter process's stdout and stderr.
handle.exitCode in cross-spawn-spawner.ts waits on the Node.js close event (not exit). Node's close only fires after all stdio stream file descriptors are closed — including those held by any grandchild processes that inherited them.
When a formatter like pants spawns worker subprocesses (JVM workers, Pex resolvers, etc.), those grandchildren inherit the open pipe FDs. Even after pants itself exits (triggering exit), close never fires because the grandchildren still hold the pipes open. The Deferred that handle.exitCode awaits is never resolved, and the tool call hangs forever.
The formatter output is not read anywhere (handle.stdout / handle.stderr are never consumed), so the pipe buffers also fill up, compounding the issue.
Fix
Restore stdout: "ignore" and stderr: "ignore" in packages/opencode/src/format/index.ts:
With "ignore", no pipes are created for stdout/stderr, so close fires immediately when the formatter process exits regardless of any grandchild processes still running.
Reproduction
Configure a custom formatter in .opencode/opencode.jsonc that uses a tool which spawns background workers (e.g. pants --no-pantsd fix \$FILE), then have opencode edit any matching file. The edit tool call will hang indefinitely.
Impact
Any project using a custom formatter that spawns background child processes will see all edit/write operations hang permanently after the first file edit.
Activity