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

  1. Enable archiveExisting: true in the security output options so prior contents are renamed aside (`.archive-<suffix>`).
  2. Empty the directory manually (`rm -rf <path>/*`) and re-run.
  3. 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

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


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