can1357/oh-my-pi · error · Error
Security output path exists and is not a directory
Error message
Security output path exists and is not a directory
What it means
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.
Source
Thrown at packages/coding-agent/src/security/preflight.ts:257
return results.sort((left, right) => left.path.localeCompare(right.path));
}
async function normalizeOutput(
repositoryRoot: string,
outputRoot: string,
archiveExisting: boolean,
): Promise<SecurityOutputPlan> {
const requested = path.resolve(outputRoot);
const parent = await fs.realpath(path.dirname(requested));
const canonicalCandidate = path.join(parent, path.basename(requested));
if (pathIsWithin(canonicalCandidate, repositoryRoot)) {
throw new Error("Security output directory must be outside the scanned repository");
}
let existingState: SecurityOutputPlan["existingState"] = "absent";
try {
const stats = await fs.lstat(canonicalCandidate);
if (stats.isSymbolicLink()) throw new Error("Security output directory must not be a symbolic link");
if (!stats.isDirectory()) throw new Error("Security output path exists and is not a directory");
const real = await fs.realpath(canonicalCandidate);
if (real !== canonicalCandidate) throw new Error("Security output directory does not have a canonical identity");
const entries = await fs.readdir(canonicalCandidate);
existingState = entries.length === 0 ? "empty" : "archivable";
if (entries.length > 0 && !archiveExisting) {
throw new Error("Security output directory is not empty; enable archiveExisting or choose another directory");
}
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
await fs.mkdir(canonicalCandidate, { recursive: false, mode: 0o700 });
existingState = "empty";
}
if (process.platform !== "win32") await fs.chmod(canonicalCandidate, 0o700);
return { root: canonicalCandidate, archiveExisting, existingState };
}
export interface PreparedSecurityOutput {
root: string;View on GitHub (pinned to 9690622007)
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.
Example fix
// shell # before: ./sec-out is a regular file mv sec-out sec-out.report.bak mkdir sec-out // after: ./sec-out is a directory
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from "node:fs/promises";
try {
const st = await fs.lstat(outputPath);
if (st.isFile()) {
await fs.rename(outputPath, `${outputPath}.bak`);
await fs.mkdir(outputPath);
}
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
} Try / catch
try {
await output(options);
} catch (err) {
if ((err as Error).message.includes("exists and is not a directory")) {
// move the file aside or ask the user for a different path before retrying
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Security knowledge base is not a file: ${input}
- Security output directory must be outside the scanned reposi
- cannot access {}: Not a directory
- failed to access {0}: Not a directory
- Managed skill "${name}" SKILL.md is not a regular file; refu
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/0b7eee0e302e53a4.
Report an issue: GitHub.