can1357/oh-my-pi · error · Error
Security output directory is not empty; enable archiveExisti
Error message
Security output directory is not empty; enable archiveExisting or choose another directory
What it means
normalizeOutput refuses to proceed when the output directory already exists, is a real directory, but is non-empty and archiveExisting was not enabled. Existing content would be clobbered or mixed with new scan output, so the caller must explicitly opt into archiving the old contents or pick an empty directory.
Source
Thrown at packages/coding-agent/src/security/preflight.ts:263
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;
archivedTo?: string;
}
export async function prepareSecurityOutputDirectory(
output: SecurityOutputPlan,
archiveSuffix: string = Bun.randomUUIDv7(),View on GitHub (pinned to 9690622007)
Solutions
- Enable archiveExisting: true in the security output options so prior contents are renamed aside (`.archive-<suffix>`).
- Empty the directory manually (`rm -rf <path>/*`) and re-run.
- Choose a fresh outputRoot path for the new run.
Example fix
// before
const plan = await output({ outputRoot, repositoryRoot });
// after
const plan = await output({ outputRoot, repositoryRoot, archiveExisting: true }); Defensive patterns
Strategy: validation
Validate before calling
import * as fs from "node:fs/promises";
try {
const entries = await fs.readdir(outputPath);
if (entries.length > 0 && !archiveExisting) {
throw new Error(`Output dir not empty (${entries.length} entries); set archiveExisting or clean it`);
}
} 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("is not empty")) {
options.archiveExisting = true;
return output(options); // retry, archiving old contents
}
throw err;
} Prevention
- Set archiveExisting: true when reusing an output directory across runs.
- Use a timestamped or per-run output directory name.
- Check readdir length before invoking with default options.
When it happens
Trigger: Calling output()/normalizeOutput with archiveExisting=false (default) where readdir(canonicalCandidate) returns one or more entries.
Common situations: Re-running a scan against a previously used output dir; user pointed outputRoot at a general-purpose directory like ~/tmp or the project root's parent; leftover files from another tool.
Related errors
- Shared-folder destination escapes its configured root
- Security knowledge base is not a file: ${input}
- Security output directory must be outside the scanned reposi
- The managed-skills root is a symlink; refusing to operate ou
- 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/03099200c5e62adc.
Report an issue: GitHub.