{"record":{"id":"22f6f47554ca4bf4","repo":"can1357/oh-my-pi","slug":"working-directory-is-not-a-directory-commandcwd","errorCode":null,"errorMessage":"Working directory is not a directory: ${commandCwd}","messagePattern":"Working directory is not a directory: (.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/bash.ts","lineNumber":995,"sourceCode":"\n\t\t// Best-effort cache invalidation: drop github-cache rows for any issue/PR\n\t\t// number touched by a mutating `gh` subcommand inside this bash call so\n\t\t// subsequent issue:// / pr:// reads pick up the post-mutation state\n\t\t// instead of the cached pre-mutation snapshot.\n\t\tinvalidateGithubCacheForBashCommand(command);\n\n\t\tconst commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;\n\t\tlet cwdStat: fs.Stats;\n\t\ttry {\n\t\t\tcwdStat = await fs.promises.stat(commandCwd);\n\t\t} catch (err) {\n\t\t\tif (isEnoent(err)) {\n\t\t\t\tthrow new ToolError(`Working directory does not exist: ${commandCwd}`);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t\tif (!cwdStat.isDirectory()) {\n\t\t\tthrow new ToolError(`Working directory is not a directory: ${commandCwd}`);\n\t\t}\n\n\t\t// A timeout of 0 is an explicit long-running-command contract: the user\n\t\t// must still cancel the call or job, but OMP does not impose a deadline.\n\t\tconst requestedTimeoutSec = rawTimeout;\n\t\tconst timeoutDisabled = requestedTimeoutSec === 0;\n\t\tconst maxTimeout = this.session.settings.get(\"tools.maxTimeout\");\n\t\tconst timeoutSec = timeoutDisabled ? undefined : clampTimeout(\"bash\", requestedTimeoutSec, maxTimeout);\n\t\tconst timeoutMs = timeoutSec === undefined ? undefined : timeoutSec * 1000;\n\t\tconst pendingNotices: string[] = [];\n\t\tif (timeoutSec !== undefined) {\n\t\t\tconst timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec, maxTimeout);\n\t\t\tif (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);\n\t\t}\n\n\t\tif (asyncRequested) {\n\t\t\tif (!this.session.asyncJobManager) {\n\t\t\t\tthrow new ToolError(\"Async job manager unavailable for this session.\");","sourceCodeStart":977,"sourceCodeEnd":1013,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/bash.ts#L977-L1013","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Point cwd at the directory, not a file inside it (strip the filename or use its dirname).","Verify with `ls -la <path>` / stat whether the target is a directory or file.","If a symlink flipped type unexpectedly, inspect what replaced it and re-clone/restore the directory."],"exampleFix":"// before: cwd is a file\nawait bash.execute(id, { command: \"npm test\", cwd: \"/repo/packages/app/package.json\" });\n\n// after: use the containing directory\nawait bash.execute(id, { command: \"npm test\", cwd: \"/repo/packages/app\" });","handlingStrategy":"validation","validationCode":"const dir = cwd ?? session.cwd;\nif ((await stat(dir)).isDirectory()) {\n  await bash.execute(id, { command, cwd: dir });\n}","typeGuard":"function isDirectoryStat(s: fs.Stats): boolean {\n  return s.isDirectory();\n}","tryCatchPattern":"try {\n  await bash.execute(id, { command, cwd });\n} catch (e) {\n  if (e instanceof ToolError && e.message.includes(\"is not a directory\")) {\n    const dir = path.dirname(cwd!);\n    return bash.execute(id, { command, cwd: dir });\n  }\n  throw e;\n}","preventionTips":["Pass directories, never files, as cwd — strip filenames with path.dirname.","Verify symlink targets still resolve to directories before use.","Check `ls -la` output when paths were created by other tooling."],"tags":["filesystem","path","cwd"],"backgroundTag":"not-a-directory","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}