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
- Verify the agent command exists and is executable (`which <command>`) — a failed spawn yields null streams.
- Ensure the backend spawns with stdio: ['pipe','pipe','pipe'] so all three streams are created.
- 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
- Verify the agent command is on PATH before launching (`which <cmd>`).
- Always spawn with stdio: ['pipe','pipe','pipe'] for ACP transport.
- Listen for the child process 'error' event to catch spawn failures early.
- Test agent startup in minimal containers/sandboxes where spawn may be restricted.
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
- Backend has been disposed
- Session not started
- ACP session is not started
- ACP session not started
- Gemini backend or session not initialized
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/9e04b89333b6a84b.
Report an issue: GitHub.