CLI Shell tool: a deleted working directory sticks, and every later command returns "no exit status"

Where does the bug appear (feature/product)?

Cursor CLI

Describe the Bug

The Shell tool starts each command in the folder where the previous command ended. If a command deletes that folder, every later Shell call fails, and the session does not recover on its own. The error text tells the model to restart the environment. It never says the folder is gone.

This is common. A temp-dir cleanup does it:

T=(mktemp −d) cd “T” && … && rm -rf “$T”

What we hit

Four sessions were stuck at once. One of them had been stuck for about 23 hours, with 89 failed commands. Nothing in the tool result named the missing directory. A failed command cannot change the saved folder, so the agent stays stuck until the session is restarted, or until some later call passes a working_directory that still exists.

What we changed locally

A small patch on that path. Before spawning, if the saved folder does not exist, reset it to the workspace root and add a stderr line saying the previous folder was gone. Only fail when the model explicitly asked for a folder that is missing, and then say that the folder is missing.

Tested with the three-call repro. Call 1 still succeeds. Calls 2 and 3 run. The model sees the notice. A Cursor update will overwrite the patch.

Suggested product fix

Must

• Before spawn, check that the folder still exists.
• If the saved folder is gone and the model did not name one, reset this.cwd to the workspace root (or $HOME) and put one line in the result: the previous working directory no longer exists, and the command ran in the fallback.
• If the model did name a missing folder, fail that call only, and say the path is missing. Do not leave the stale path in place for the next call.
• Pass the real spawn error through, including ENOENT and the path. Do not collapse it to “no exit status”.
• Do the same in zsh, zsh-light, PowerShell, and the generic terminal executor. They share the pattern.

Should

• Do not describe spawn bash ENOENT as if bash itself were missing when cwd is the path that does not exist.
• A failed spawn must not be the only thing that can clear this.cwd. The saved folder has to move even when the state dump never runs.

Steps to Reproduce

Steps to reproduce

One prompt, about 15 seconds. No working_directory on any call.

mkdir -p /tmp/repro && cd /tmp/repro cursor-agent -p --force --trust --model composer-2.5 --output-format stream-json
‘Make three separate Shell calls without working_directory:
1: D=(mktemp −d) cd “D” && rm -rf “$D” && echo removed
2: date -u
3: pwd’

Expected

Call 1 removes the temp dir and succeeds. Calls 2 and 3 run, in the workspace folder. If the saved folder is gone, the result says so, and the next command does not inherit it.

Actual

Call 1 succeeds. Calls 2 and 3 return about 100 ms after starting, with no exit code:

{"spawnError":{"command":"","workingDirectory":"","error":"The shell command returned no exit status, so its result is unknown — do not assume it ran or succeeded. If this repeats, the execution environment may need to be restarted."}}

Root cause, from the shipped bundle

Three separate facts. All three have to be true for the session to stick.

1. The spawn uses a folder that is already gone. In BashState.execute, the spawn folder is r?.workingDirectory ?? this.cwd. After call 1, this.cwd is taken from the dumped $PWD, which is the deleted temp dir. child_process.spawn(..., { cwd }) on a missing folder emits error (ENOENT, reported as spawn bash ENOENT, which is misleading because bash exists) and close(-2). It never emits exit. The exit code stays null, so the tool reports "no exit status".

2. The saved folder is only updated after a successful state dump. The failed spawn produces none. this.cwd keeps pointing at the missing folder.

3. Every later Shell call goes through that value. LocalShellExecutor passes getCwd(), the same stale folder, as workingDirectory. A call that does not set working_directory cannot escape.

The same workingDirectory ?? this.cwd pattern is in zsh, zsh-light, PowerShell, and the generic terminal executor. This is not bash-only.





### Operating System
Linux

### Version Information
cursor-agent 2026.09.26-dd393fe
Linux (Fedora, kernel 7.2), bash
Modes: cursor-agent acp, and cursor-agent -p
The same code path is in both.







### Does this stop you from using Cursor
Sometimes - I can sometimes use Cursor

Hey @PedroACFerreira, thanks for the report and the root-cause notes!

We were able to reproduce this: once the folder the last command ended in is deleted, later Shell calls without a working directory keep trying to start there and fail with the “no exit status” message. I’ve flagged it to the team.

In the meantime, two things help:

  1. To unstick a session without restarting it, tell the agent to run its next command with working_directory set to the project root. One successful call with an existing folder resets the saved location, and later calls work normally again.
  2. To avoid it, have temp-folder work run in a subshell so the cd doesn’t carry over, for example T=$(mktemp -d) && ( cd "$T" && ... ) && rm -rf "$T". You can put that in a project rule or AGENTS.md so the agent follows it by default.

The same bug is in the Cursor SDK’s local runtime (@cursor/sdk 1.0.35 and 1.0.36, same code in both). There the stuck calls return Command failed to spawn: spawn /run/current-system/sw/bin/bash ENOENT, which blames bash rather than the missing folder. The run stream emits running for those calls and never a completion.

Minimal SDK repro (1.0.36, Node 24, Linux), about 15 s:

import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent } from "@cursor/sdk";

const agent = await Agent.create({
  model: { id: "composer-2.5" },
  local: { cwd: mkdtempSync(join(tmpdir(), "ws-")), settingSources: [], sandboxOptions: { enabled: false } },
});
const run = await agent.send(`Make exactly three separate calls to your shell tool, one command per call, one at a time (wait for each result before the next call), and do not pass a working directory to any of them:
1) d=$(mktemp -d /tmp/cwdtest-XXXX) && cd "$d" && rmdir "$d" && echo "removed $d"
2) echo hello
3) pwd
Do not run anything else. Then report exactly what each call returned.`);
for await (const event of run.stream()) {
  if (event.type === "tool_call") console.log(event.status, JSON.stringify(event.args), JSON.stringify(event.result ?? ""));
}
console.log((await run.wait()).result, run.requestId);
agent.close();

Call 1 prints removed /tmp/cwdtest-hBlr; calls 2 and 3 get Command failed to spawn: spawn /run/current-system/sw/bin/bash ENOENT. It reproduced in 4 of 4 runs that read run.stream(), for example requests 81567117-11ac-4a9e-9eeb-d367da1b6980 and 45377d23-1974-459f-9276-cfd9db6236be (2026-10-07, privacy mode on; the failure is client-side anyway). In 3 runs that only called wait(), the cd from call 1 didn’t carry over at all, so nothing failed.

In T3 Code, which embeds the SDK, this hit us 11 times in 4 episodes on Oct 4–5, and because the error blames bash, the model tends to give up on the shell instead of changing folder.

The site is the same as in your root cause: BashState, ZshState and ZshLightState .execute in 1.0.36 dist/cjs/867.js (689.js in 1.0.35) spawn in r?.workingDirectory??this.cwd. resetSessionCwdAfterHopCdFailure only handles the “working directory is not accessible” (FUSE) error, not a deleted folder.

Our in-process workaround at those three sites differs from yours in one way: before spawning we stat the folder, and on ENOENT/ENOTDIR we spawn in the workspace root (else the nearest existing parent, else $HOME), but we replace the command with printf '%s\n' '<the working directory … does not exist, so this command did not run; the shell is now in …>' >&2; (exit 1). We don’t run the original command in the fallback folder, because a command written for the deleted folder (relative paths, rm -rf ./out) could do the wrong thing elsewhere. The state dump then records the new folder, so the next call works. Through T3 Code with Opus 5.5: call 2 exits 1 with that note, and call 3 (pwd) prints the workspace root.