Repository: QwenLM/qwen-code (2026-04-16T01:00:42Z)

Issue:
ui.statusLine crashes CLI with spawn EBADF on macOS, then hits ansiRegex3 is not a function
## Summary

Enabling `ui.statusLine` causes Qwen Code CLI to crash after a normal response completes.

The assistant response succeeds, but the status line refresh immediately fails with:

```txt
Error: spawn EBADF
    at ChildProcess.spawn (node:internal/child_process:420:11)
    at spawn (node:child_process:787:9)
    at Object.execFile (node:child_process:349:17)
    at exec (node:child_process:236:25)
    at file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:502641:19
    at Timeout._onTimeout (file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:502672:7)
    at listOnTimeout (node:internal/timers:585:17)
    at process.processTimers (node:internal/timers:521:7)
```

Then error rendering appears to hit a second issue:

```txt
TypeError: ansiRegex3 is not a function
    at module2.exports (file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:424225:82)
    at stringWidth (file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:424286:17)
    at file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:424335:17
    at Array.map (<anonymous>)
    at ansiAlign (file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:424333:19)
    at makeContentText (file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:511508:40)
    at boxen (file:///opt/homebrew/lib/node_modules/@qwen-code/qwen-code/cli.js:511676:10)
```

## Minimal reproduction

Use this config:

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "echo qwen-ok"
    }
  }
}
```

Run:

```bash
qwen -d -i "hello"
```

Observed:

1. Qwen starts normally
2. The prompt is processed normally
3. The assistant returns a normal response
4. Right after response completion, the CLI crashes with `spawn EBADF`

## Notes

This is not specific to a custom shell script.

I reproduced the same crash with both:

```json
"command": "echo qwen-ok"
```

and

```json
"command": "bash ~/.qwen/statusline-command.sh"
```

If I remove `ui.statusLine` entirely, the CLI works normally.

## Relevant code path

The bundled CLI appears to run the status line command here:

- `cli.js:502520`
- `cli.js:502658`
- `cli.js:502679`

It uses `child_process.exec(...)` and then writes JSON to `child.stdin`.

The secondary rendering failure appears around:

- `cli.js:424225`

## Environment

- Qwen Code: `0.14.4`
- Node.js: `v22.22.1`
- OS: macOS Darwin `25.3.0`
- Arch: `arm64`
- Terminal: Ghostty
- `TERM=xterm-ghostty`

## Expected behavior

A failing `statusLine` command should not crash the whole CLI.

At minimum, Qwen Code should ignore the status line failure and continue running.

## Workaround

Removing `ui.statusLine` from config avoids the crash completely.

## Additional observations

- The crash reproduces with a trivial status line command (`echo qwen-ok`), so it does not appear to depend on shell script complexity, `jq`, or Git.
- The main assistant response completes successfully before the crash, so the failure seems to happen specifically during the post-response status line refresh.
- Removing `ui.statusLine` makes the CLI stable again.
- This was reproduced on Node.js `v22.22.1`, so it may be related to Node 22 child process / stdio behavior on macOS.
- The terminal in use was Ghostty (`TERM=xterm-ghostty`), which may also be relevant if the issue depends on TTY handling.



Discussion:

Patch:
diff --git a/packages/cli/src/ui/hooks/useStatusLine.ts b/packages/cli/src/ui/hooks/useStatusLine.ts
index 30173761e7..1fd0daca85 100644
--- a/packages/cli/src/ui/hooks/useStatusLine.ts
+++ b/packages/cli/src/ui/hooks/useStatusLine.ts
@@ -269,21 +269,33 @@ export function useStatusLine(): {
     // Bump generation so earlier in-flight callbacks are ignored.
     const gen = ++generationRef.current;
 
-    const child = exec(
-      cmd,
-      { cwd: cfg.getTargetDir(), timeout: 5000, maxBuffer: 1024 * 10 },
-      (error, stdout) => {
-        if (gen !== generationRef.current) return; // stale
-        activeChildRef.current = undefined;
-        if (!error && stdout) {
-          // Strip only the trailing newline to preserve intentional whitespace.
-          const line = stdout.replace(/\r?\n$/, '').split(/\r?\n/, 1)[0];
-          setOutput(line || null);
-        } else {
-          setOutput(null);
-        }
-      },
-    );
+    // exec() can throw synchronously: libuv reports a handful of spawn
+    // errors (EACCES, ENOENT, …) via the async 'error' event, but anything
+    // else — including EBADF, reported on macOS Node 22 in issue #3264 — is
+    // thrown from ChildProcess.spawn. Without this guard the throw escapes
+    // the setTimeout callback and crashes the CLI as uncaughtException.
+    let child: ChildProcess;
+    try {
+      child = exec(
+        cmd,
+        { cwd: cfg.getTargetDir(), timeout: 5000, maxBuffer: 1024 * 10 },
+        (error, stdout) => {
+          if (gen !== generationRef.current) return; // stale
+          activeChildRef.current = undefined;
+          if (!error && stdout) {
+            // Strip only the trailing newline to preserve intentional whitespace.
+            const line = stdout.replace(/\r?\n$/, '').split(/\r?\n/, 1)[0];
+            setOutput(line || null);
+          } else {
+            setOutput(null);
+          }
+        },
+      );
+    } catch (err) {
+      debugLog.error('statusline exec error:', (err as Error).message);
+      setOutput(null);
+      return;
+    }
 
     activeChildRef.current = child;
 


Test patch:
diff --git a/packages/cli/src/ui/hooks/useStatusLine.test.ts b/packages/cli/src/ui/hooks/useStatusLine.test.ts
index 369fb2df6d..af8f06c5da 100644
--- a/packages/cli/src/ui/hooks/useStatusLine.test.ts
+++ b/packages/cli/src/ui/hooks/useStatusLine.test.ts
@@ -581,4 +581,57 @@ describe('useStatusLine', () => {
       expect(firstKill).toHaveBeenCalled();
     });
   });
+
+  // --- Spawn failure handling (issue #3264) ---
+  //
+  // On macOS with Node 22, exec() can throw synchronously with EBADF when
+  // stdio pipe setup fails. The throw must not escape doUpdate() — or the
+  // setTimeout callback — or the whole CLI crashes.
+
+  describe('spawn failure handling', () => {
+    it('does not crash when exec throws synchronously (EBADF)', () => {
+      vi.mocked(child_process.exec).mockImplementationOnce((() => {
+        const err = new Error('spawn EBADF') as NodeJS.ErrnoException;
+        err.code = 'EBADF';
+        throw err;
+      }) as unknown as typeof child_process.exec);
+
+      setStatusLineConfig({ type: 'command', command: 'echo test' });
+
+      let result: { current: { text: string | null } } | undefined;
+      expect(() => {
+        result = renderHook(() => useStatusLine()).result;
+      }).not.toThrow();
+      expect(result!.current.text).toBeNull();
+    });
+
+    it('recovers on subsequent state changes after a sync exec failure', async () => {
+      // First call throws, subsequent calls succeed with the default mock.
+      // Verifies activeChildRef and generationRef don't get wedged.
+      vi.mocked(child_process.exec).mockImplementationOnce((() => {
+        const err = new Error('spawn EBADF') as NodeJS.ErrnoException;
+        err.code = 'EBADF';
+        throw err;
+      }) as unknown as typeof child_process.exec);
+
+      setStatusLineConfig({ type: 'command', command: 'echo test' });
+      const { result, rerender } = renderHook(() => useStatusLine());
+
+      expect(result.current.text).toBeNull();
+      expect(child_process.exec).toHaveBeenCalledTimes(1);
+
+      // Trigger a re-execution via state change — should use the default mock.
+      mockUIState.currentModel = 'new-model';
+      rerender();
+      await act(async () => {
+        vi.advanceTimersByTime(300);
+      });
+
+      expect(child_process.exec).toHaveBeenCalledTimes(2);
+      await act(async () => {
+        execCallback(null, 'recovered\n', '');
+      });
+      expect(result.current.text).toBe('recovered');
+    });
+  });
 });