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
- Move the output directory outside the repository, e.g. /tmp/security-report or ~/security-reports/<project>.
- Add the path to the config's outputRoot option instead of relying on a repo-relative default.
- 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
- Default outputRoot to an absolute path outside any repo (/tmp, ~/security-reports).
- Never use repo-relative defaults for scan output.
- Check containment with path.relative before configuring.
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
- Security knowledge base is not a file: ${input}
- Shared-folder destination escapes its configured root
- Security output path exists and is not a directory
- Security output directory does not have a canonical identity
- Security output directory is not empty; enable archiveExisti
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/467bbb6e9734fcc1.
Report an issue: GitHub.