can1357/oh-my-pi · error · Error

Security output directory must be outside the scanned reposi

Error message

Security output directory must be outside the scanned repository

What it means

normalizeOutput validates that the configured security-scan output directory lives outside the repository being scanned. The requested path is resolved and canonicalized against its parent, then pathIsWithin() checks containment against repositoryRoot; an output dir inside the repo would let scan results pollute (or be re-scanned from) the tree, so it is rejected.

Source

Thrown at packages/coding-agent/src/security/preflight.ts:251

		const canonical = await fs.realpath(path.resolve(baseDirectory, input));
		const stats = await fs.stat(canonical);
		if (!stats.isFile()) throw new Error(`Security knowledge base is not a file: ${input}`);
		const digest = await hashFile(canonical);
		results.push({ path: canonical, sha256: digest.sha256, size: digest.size });
	}
	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";
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Move the output directory outside the repository, e.g. /tmp/security-report or ~/security-reports/<project>.
  2. Add the path to the config's outputRoot option instead of relying on a repo-relative default.
  3. Verify resolution: run `realpath -m <your-output-path>` and confirm the result is not under the repo root.

Example fix

// before
const plan = await output({ outputRoot: "./reports", repositoryRoot });
// after
const plan = await output({ outputRoot: "/tmp/security-reports/myproj", repositoryRoot });
Defensive patterns

Strategy: validation

Validate before calling

import * as path from "node:path";
import * as fs from "node:fs/promises";
const requested = path.resolve(outputRoot);
const real = path.join(await fs.realpath(path.dirname(requested)), path.basename(requested));
const rel = path.relative(repositoryRoot, real);
if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) {
  throw new Error("Choose an outputRoot outside the repository");
}

Try / catch

try {
  await output(options);
} catch (err) {
  if ((err as Error).message.includes("must be outside the scanned repository")) {
    // prompt user for an external directory, e.g. /tmp or ~
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling output()/normalizeOutput with an outputRoot that resolves inside repositoryRoot — e.g. outputRoot="./security-report" in a repo at /repo, or any path like /repo/../repo/out.

Common situations: Developer sets the report dir next to the code inside the project ('./reports', 'dist/security'); relative paths accidentally resolve into the repo because cwd is the repo root; symlink or '..' tricks that fold back inside the repo.

Related errors


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