diff --git a/packages/app/src/app/agent/new.tsx b/packages/app/src/app/agent/new.tsx
index 5fb67b83f..94cbfb059 100644
--- a/packages/app/src/app/agent/new.tsx
+++ b/packages/app/src/app/agent/new.tsx
@@ -244,6 +244,9 @@ export default function DraftAgentScreen() {
const isNonGitDirectory =
repoRequestStatus === "error" &&
/not in a git repository/i.test(repoRequestError?.message ?? "");
+ const isDirectoryNotExists =
+ repoRequestStatus === "error" &&
+ /does not exist|no such file or directory|ENOENT/i.test(repoRequestError?.message ?? "");
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
? "idle"
: repoAvailabilityError
@@ -490,6 +493,10 @@ export default function DraftAgentScreen() {
setErrorMessage("Working directory is required");
throw new Error("Working directory is required");
}
+ if (isDirectoryNotExists) {
+ setErrorMessage("Working directory does not exist on the selected host");
+ throw new Error("Working directory does not exist on the selected host");
+ }
if (!trimmedPrompt) {
setErrorMessage("Initial prompt is required");
throw new Error("Initial prompt is required");
@@ -562,6 +569,7 @@ export default function DraftAgentScreen() {
createNewBranch,
createWorktree,
gitBlockingError,
+ isDirectoryNotExists,
isLoading,
isNonGitDirectory,
modeOptions,
@@ -692,6 +700,13 @@ export default function DraftAgentScreen() {
wrapInContainer={false}
renderTrigger={renderDropdownTrigger}
/>
+ {isDirectoryNotExists && (
+
+
+ Directory does not exist on the selected host
+
+
+ )}
{renderConfigRow({
label: "Agent",
value: `${providerDefinitions.find((p) => p.id === selectedProvider)?.label ?? selectedProvider} ยท ${selectedModel || "auto"}`,
@@ -1037,6 +1052,17 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.destructiveForeground,
fontSize: theme.fontSize.sm,
},
+ warningContainer: {
+ paddingHorizontal: theme.spacing[3],
+ paddingVertical: theme.spacing[2],
+ borderRadius: theme.borderRadius.md,
+ backgroundColor: theme.colors.palette.yellow[400],
+ },
+ warningText: {
+ color: "#000000",
+ fontSize: theme.fontSize.sm,
+ fontWeight: theme.fontWeight.semibold,
+ },
hostBadge: {
flexDirection: "row",
alignItems: "center",
diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts
index 16dbc8afd..1e01f0c6f 100644
--- a/packages/server/src/server/daemon.e2e.test.ts
+++ b/packages/server/src/server/daemon.e2e.test.ts
@@ -82,6 +82,19 @@ describe("daemon E2E", () => {
expect(hasAssistantMessage).toBe(true);
}, 180000); // 3 minute timeout for E2E test
+ test("fails to create agent with non-existent cwd", async () => {
+ const nonExistentCwd = "/this/path/does/not/exist/12345";
+
+ const result = await ctx.client.createAgentExpectFail({
+ provider: "codex",
+ cwd: nonExistentCwd,
+ title: "Should Fail Agent",
+ });
+
+ expect(result.error).toContain("Working directory does not exist");
+ expect(result.error).toContain(nonExistentCwd);
+ });
+
describe("permission flow: Codex", () => {
test(
"approves permission and executes command",
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index ae286b502..5b3d68a67 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from "uuid";
-import { readFile, mkdir, writeFile } from "fs/promises";
+import { readFile, mkdir, writeFile, stat } from "fs/promises";
import { exec } from "child_process";
import { promisify, inspect } from "util";
import { join } from "path";
@@ -1273,6 +1273,24 @@ export class Session {
);
try {
+ // Validate that the working directory exists
+ const resolvedCwd = expandTilde(config.cwd);
+ try {
+ const stats = await stat(resolvedCwd);
+ if (!stats.isDirectory()) {
+ throw new Error(
+ `Working directory is not a directory: ${config.cwd}`
+ );
+ }
+ } catch (statError: any) {
+ if (statError.code === "ENOENT") {
+ throw new Error(
+ `Working directory does not exist: ${config.cwd}`
+ );
+ }
+ throw statError;
+ }
+
const sessionConfig = await this.buildAgentSessionConfig(
config,
git,
diff --git a/packages/server/src/server/test-utils/daemon-client.ts b/packages/server/src/server/test-utils/daemon-client.ts
index b90f61545..2e7fc33e8 100644
--- a/packages/server/src/server/test-utils/daemon-client.ts
+++ b/packages/server/src/server/test-utils/daemon-client.ts
@@ -197,6 +197,50 @@ export class DaemonClient {
); // 60 second timeout for initialization
}
+ /**
+ * Create an agent and expect it to fail.
+ * Returns the error message from the failure.
+ */
+ async createAgentExpectFail(
+ options: CreateAgentOptions
+ ): Promise<{ error: string }> {
+ const requestId = nanoid();
+
+ // Record the current queue position so we only check NEW messages
+ const startPosition = this.messageQueue.length;
+
+ this.send({
+ type: "create_agent_request",
+ requestId,
+ config: {
+ provider: options.provider,
+ cwd: options.cwd,
+ title: options.title,
+ model: options.model,
+ modeId: options.modeId,
+ mcpServers: options.mcpServers,
+ extra: options.extra,
+ },
+ initialPrompt: options.initialPrompt,
+ });
+
+ // Wait for either agent_create_failed status or timeout
+ return this.waitFor(
+ (msg) => {
+ if (
+ msg.type === "status" &&
+ msg.payload.status === "agent_create_failed" &&
+ msg.payload.requestId === requestId
+ ) {
+ return { error: String(msg.payload.error ?? "Unknown error") };
+ }
+ return null;
+ },
+ 10000,
+ { skipQueueBefore: startPosition }
+ );
+ }
+
async deleteAgent(agentId: string): Promise {
this.send({ type: "delete_agent_request", agentId });
await this.waitFor((msg) => {