From 1432fd91d2e7ff09961785bc96107f1d368e19e0 Mon Sep 17 00:00:00 2001 From: Robin <7543162+robinwander@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:48:10 -0600 Subject: [PATCH] fix(coding-agent): improve bash tool error handling (#479) - Validate working directory exists before spawning to provide clear error message - Add spawn error handler to prevent uncaught exceptions when shell not found or cwd invalid - Add tests for both error scenarios Without these fixes, spawn errors (e.g., ENOENT from missing cwd or shell) would cause uncaught exceptions that crash the entire agent session instead of being returned as clean tool errors. Co-authored-by: robinwander --- packages/coding-agent/src/core/tools/bash.ts | 18 +++++++++++++- packages/coding-agent/test/tools.test.ts | 26 ++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 9d851922..a4da4c2b 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -1,5 +1,5 @@ import { randomBytes } from "node:crypto"; -import { createWriteStream } from "node:fs"; +import { createWriteStream, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; @@ -40,6 +40,11 @@ export function createBashTool(cwd: string): AgentTool { ) => { return new Promise((resolve, reject) => { const { shell, args } = getShellConfig(); + + if (!existsSync(cwd)) { + throw new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`); + } + const child = spawn(shell, [...args, command], { cwd, detached: true, @@ -119,6 +124,17 @@ export function createBashTool(cwd: string): AgentTool { child.stderr.on("data", handleData); } + // Handle shell spawn errors to prevent session from crashing + child.on("error", (err) => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (signal) { + signal.removeEventListener("abort", onAbort); + } + reject(err); + }); + // Handle process exit child.on("close", (code) => { if (timeoutHandle) { diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index d4601eb9..fac3f61a 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -1,14 +1,15 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { bashTool } from "../src/core/tools/bash.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { bashTool, createBashTool } from "../src/core/tools/bash.js"; import { editTool } from "../src/core/tools/edit.js"; import { findTool } from "../src/core/tools/find.js"; import { grepTool } from "../src/core/tools/grep.js"; import { lsTool } from "../src/core/tools/ls.js"; import { readTool } from "../src/core/tools/read.js"; import { writeTool } from "../src/core/tools/write.js"; +import * as shellModule from "../src/utils/shell.js"; // Helper to extract text from content blocks function getTextOutput(result: any): string { @@ -277,6 +278,27 @@ describe("Coding Agent Tools", () => { /timed out/i, ); }); + + it("should throw error when cwd does not exist", async () => { + const nonexistentCwd = "/this/directory/definitely/does/not/exist/12345"; + + const bashToolWithBadCwd = createBashTool(nonexistentCwd); + + await expect(bashToolWithBadCwd.execute("test-call-11", { command: "echo test" })).rejects.toThrow( + /Working directory does not exist/, + ); + }); + + it("should handle process spawn errors", async () => { + vi.spyOn(shellModule, "getShellConfig").mockReturnValueOnce({ + shell: "/nonexistent-shell-path-xyz123", + args: ["-c"], + }); + + const bashWithBadShell = createBashTool(testDir); + + await expect(bashWithBadShell.execute("test-call-12", { command: "echo test" })).rejects.toThrow(/ENOENT/); + }); }); describe("grep tool", () => {