can1357/oh-my-pi · error · ToolError

Working directory is not a directory: ${commandCwd}

Error message

Working directory is not a directory: ${commandCwd}

What it means

After a successful stat of the resolved working directory, the tool checks isDirectory(). If the path exists but is a file, symlink-to-file, socket, etc., it cannot serve as a cwd and the tool throws this ToolError. This complements the ENOENT check: the path is real but the wrong kind of node.

Source

Thrown at packages/coding-agent/src/tools/bash.ts:995

		// Best-effort cache invalidation: drop github-cache rows for any issue/PR
		// number touched by a mutating `gh` subcommand inside this bash call so
		// subsequent issue:// / pr:// reads pick up the post-mutation state
		// instead of the cached pre-mutation snapshot.
		invalidateGithubCacheForBashCommand(command);

		const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
		let cwdStat: fs.Stats;
		try {
			cwdStat = await fs.promises.stat(commandCwd);
		} catch (err) {
			if (isEnoent(err)) {
				throw new ToolError(`Working directory does not exist: ${commandCwd}`);
			}
			throw err;
		}
		if (!cwdStat.isDirectory()) {
			throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
		}

		// A timeout of 0 is an explicit long-running-command contract: the user
		// must still cancel the call or job, but OMP does not impose a deadline.
		const requestedTimeoutSec = rawTimeout;
		const timeoutDisabled = requestedTimeoutSec === 0;
		const maxTimeout = this.session.settings.get("tools.maxTimeout");
		const timeoutSec = timeoutDisabled ? undefined : clampTimeout("bash", requestedTimeoutSec, maxTimeout);
		const timeoutMs = timeoutSec === undefined ? undefined : timeoutSec * 1000;
		const pendingNotices: string[] = [];
		if (timeoutSec !== undefined) {
			const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec, maxTimeout);
			if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
		}

		if (asyncRequested) {
			if (!this.session.asyncJobManager) {
				throw new ToolError("Async job manager unavailable for this session.");

View on GitHub (pinned to 9690622007)

Solutions

  1. Point cwd at the directory, not a file inside it (strip the filename or use its dirname).
  2. Verify with `ls -la <path>` / stat whether the target is a directory or file.
  3. If a symlink flipped type unexpectedly, inspect what replaced it and re-clone/restore the directory.

Example fix

// before: cwd is a file
await bash.execute(id, { command: "npm test", cwd: "/repo/packages/app/package.json" });

// after: use the containing directory
await bash.execute(id, { command: "npm test", cwd: "/repo/packages/app" });
Defensive patterns

Strategy: validation

Validate before calling

const dir = cwd ?? session.cwd;
if ((await stat(dir)).isDirectory()) {
  await bash.execute(id, { command, cwd: dir });
}

Type guard

function isDirectoryStat(s: fs.Stats): boolean {
  return s.isDirectory();
}

Try / catch

try {
  await bash.execute(id, { command, cwd });
} catch (e) {
  if (e instanceof ToolError && e.message.includes("is not a directory")) {
    const dir = path.dirname(cwd!);
    return bash.execute(id, { command, cwd: dir });
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing cwd pointing at a regular file (e.g. /path/to/package.json instead of the package dir); a leading `cd <file> && ...` where the token resolves to a file; a path that used to be a directory but was replaced by a file.

Common situations: Agents copying a file path into the cwd field by mistake; confusion between a repo path and its config file; symlink targets changed by tooling.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/22f6f47554ca4bf4. Report an issue: GitHub.