slopus/happy · error · Error

Failed to create stdio pipes

Error message

Failed to create stdio pipes

What it means

startSession() spawns the agent as a child process and verifies that stdin, stdout, and stderr streams are all available on the returned ChildProcess before wiring ACP stdio transport. If any stream is missing, the process cannot speak the ACP JSON-RPC protocol over stdio, so the backend throws immediately.

Source

Thrown at packages/happy-cli/src/agent/acp/AcpBackend.ts:416

      } else {
        this.process = spawn(this.options.command, args, {
          cwd: this.options.cwd,
          env: { ...process.env, ...this.options.env },
          // Use 'pipe' for all stdio to capture output without printing to console
          // stdout and stderr will be handled by our event listeners
          stdio: ['pipe', 'pipe', 'pipe'],
        });
      }
      
      // Ensure stderr doesn't leak to console - redirect to logger only
      // This prevents gemini CLI debug output from appearing in user's console
      if (this.process.stderr) {
        // stderr is already handled by the event listener below
        // but we ensure it doesn't go to parent's stderr
      }

      if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
        throw new Error('Failed to create stdio pipes');
      }

      let startupFailure: Error | null = null;
      let startupFailureSettled = false;
      let rejectStartupFailure: ((error: Error) => void) | null = null;
      const startupFailurePromise = new Promise<never>((_, reject) => {
        rejectStartupFailure = (error: Error) => {
          if (startupFailureSettled) {
            return;
          }
          startupFailureSettled = true;
          startupFailure = error;
          reject(error);
        };
      });
      const signalStartupFailure = (error: Error) => {
        rejectStartupFailure?.(error);
      };

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Verify the agent command exists and is executable (`which <command>`) — a failed spawn yields null streams.
  2. Ensure the backend spawns with stdio: ['pipe','pipe','pipe'] so all three streams are created.
  3. Log the spawn error/exit event to find the root cause; handle the spawn 'error' event before this check.

Example fix

// before
spawn(cmd, args, { stdio: 'inherit' });
// after
spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
Defensive patterns

Strategy: validation

Validate before calling

await new Promise((resolve, reject) => {
  const p = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
  p.once('error', reject); // ENOENT etc. surfaces here
  p.once('spawn', resolve);
});

Type guard

function hasStdio(p: ChildProcess): p is ChildProcess & {
  stdin: NodeJS.WriteStream; stdout: NodeJS.ReadStream; stderr: NodeJS.ReadStream;
} {
  return Boolean(p.stdin && p.stdout && p.stderr);
}

Try / catch

try {
  await backend.startSession();
} catch (err) {
  if (err instanceof Error && err.message === 'Failed to create stdio pipes') {
    console.error(`Agent command failed to spawn: verify '${agentCommand}' exists and is executable`);
  } else throw err;
}

Prevention

When it happens

Trigger: child_process spawn returned a process object with null stdin/stdout/stderr — typically when spawn failed (ENOENT on the command), the stdio option array didn't include 'pipe' for all three, or spawn options like detached/serialization misconfigured the pipes.

Common situations: Agent command not on PATH so spawn fails; custom `happy acp -- <command>` where the stdio config was altered; running in environments that restrict process spawning (sandboxes, minimal containers).

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/9e04b89333b6a84b. Report an issue: GitHub.