{"record":{"id":"0b7eee0e302e53a4","repo":"can1357/oh-my-pi","slug":"security-output-path-exists-and-is-not-a-directory","errorCode":null,"errorMessage":"Security output path exists and is not a directory","messagePattern":"Security output path exists and is not a directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/security/preflight.ts","lineNumber":257,"sourceCode":"\treturn results.sort((left, right) => left.path.localeCompare(right.path));\n}\n\nasync function normalizeOutput(\n\trepositoryRoot: string,\n\toutputRoot: string,\n\tarchiveExisting: boolean,\n): Promise<SecurityOutputPlan> {\n\tconst requested = path.resolve(outputRoot);\n\tconst parent = await fs.realpath(path.dirname(requested));\n\tconst canonicalCandidate = path.join(parent, path.basename(requested));\n\tif (pathIsWithin(canonicalCandidate, repositoryRoot)) {\n\t\tthrow new Error(\"Security output directory must be outside the scanned repository\");\n\t}\n\tlet existingState: SecurityOutputPlan[\"existingState\"] = \"absent\";\n\ttry {\n\t\tconst stats = await fs.lstat(canonicalCandidate);\n\t\tif (stats.isSymbolicLink()) throw new Error(\"Security output directory must not be a symbolic link\");\n\t\tif (!stats.isDirectory()) throw new Error(\"Security output path exists and is not a directory\");\n\t\tconst real = await fs.realpath(canonicalCandidate);\n\t\tif (real !== canonicalCandidate) throw new Error(\"Security output directory does not have a canonical identity\");\n\t\tconst entries = await fs.readdir(canonicalCandidate);\n\t\texistingState = entries.length === 0 ? \"empty\" : \"archivable\";\n\t\tif (entries.length > 0 && !archiveExisting) {\n\t\t\tthrow new Error(\"Security output directory is not empty; enable archiveExisting or choose another directory\");\n\t\t}\n\t} catch (error) {\n\t\tif (!(error instanceof Error && \"code\" in error && error.code === \"ENOENT\")) throw error;\n\t\tawait fs.mkdir(canonicalCandidate, { recursive: false, mode: 0o700 });\n\t\texistingState = \"empty\";\n\t}\n\tif (process.platform !== \"win32\") await fs.chmod(canonicalCandidate, 0o700);\n\treturn { root: canonicalCandidate, archiveExisting, existingState };\n}\n\nexport interface PreparedSecurityOutput {\n\troot: string;","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/security/preflight.ts#L239-L275","documentation":"normalizeOutput lstats the canonical output candidate and finds that an existing entry at that path is not a directory (regular file, symlink handled separately, FIFO, etc.). The output plan requires a directory (or an absent path it can create), so any non-directory existing entry is rejected.","triggerScenarios":"Calling output()/normalizeOutput where outputRoot points at an existing file — e.g. a stray `security-report` file, a tarball named like the output dir, or a created-but-empty marker file.","commonSituations":"A previous run wrote a single report file at the configured output path; shell redirect (`> out`) accidentally created the name as a file; user mistyped the path and hit an existing artifact.","solutions":["Inspect the path (`ls -l`) and remove or rename the offending non-directory entry (`mv security-report security-report.bak`).","Create a real directory at the path: `mkdir <path>`.","Point outputRoot at a different, unused directory path."],"exampleFix":"// shell\n# before: ./sec-out is a regular file\nmv sec-out sec-out.report.bak\nmkdir sec-out\n// after: ./sec-out is a directory","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\ntry {\n  const st = await fs.lstat(outputPath);\n  if (st.isFile()) {\n    await fs.rename(outputPath, `${outputPath}.bak`);\n    await fs.mkdir(outputPath);\n  }\n} catch (e) {\n  if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") throw e;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await output(options);\n} catch (err) {\n  if ((err as Error).message.includes(\"exists and is not a directory\")) {\n    // move the file aside or ask the user for a different path before retrying\n  }\n  throw err;\n}","preventionTips":["Never shell-redirect into the configured output path name.","Reserve the output directory path exclusively for scan output.","Check `ls -ld <path>` before configuring; a dash after permissions means file, d means directory."],"tags":["filesystem","validation","path"],"backgroundTag":"expected-directory-found-file","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}