santifer/career-ops · error · Error
local-parser: path escapes the project root
Error message
local-parser: path escapes the project root: ${rawPath} What it means
resolveInsideRoot() resolves a configured path against PROJECT_ROOT and follows symlinks via realpathSync, then verifies the result is still inside the project tree. This blocks path traversal (../) and symlink escapes so portals.yml can never point the local parser at arbitrary files outside the repo.
Solutions
- Place the script inside the project and use a repo-relative path, e.g. parsers/my-parser.py.
- Remove ../ segments and any absolute path from parser.command / parser.script / detected script args.
- Check for symlinks: the check uses realpathSync, so retarget any in-repo symlink to a location inside the repo.
- If the tool must run an external binary, add it to ALLOWED_INTERPRETERS in providers/local-parser.mjs rather than pointing at it by path.
Example fix
// before (portals.yml)
parser: {command: python3, args: ["../shared/parse_jobs.py"]}
// after
parser: {command: python3, args: ["parsers/parse_jobs.py"]} Defensive patterns
Strategy: validation
Validate before calling
import { realpathSync } from 'fs';
import { resolve, sep } from 'path';
const ROOT = realpathSync(process.cwd());
function assertInsideRoot(p) {
const r = realpathSync(resolve(ROOT, String(p)));
if (r !== ROOT && !r.startsWith(ROOT + sep)) throw new Error(`path escapes project root: ${p}`);
} Try / catch
try {
await localParser.fetch(entry);
} catch (e) {
if (String(e.message).includes('escapes the project root')) {
console.error(`${entry.name}: move the script into the repo and use a relative path`);
return [];
}
throw e;
} Prevention
- Use repo-relative paths for parser.command/script; never absolute paths or ../.
- Audit in-repo symlinks so realpath stays inside the project.
- Keep shared helper scripts in the repo (e.g. parsers/) instead of referencing external locations.
- Re-check paths after moving or cloning the repo to a new location.
When it happens
Trigger: parser.command or parser.script resolving outside PROJECT_ROOT — e.g. script: ../../etc/passwd, an absolute path like /usr/bin/curl, or an in-repo path whose symlink target leaves the repo.
Common situations: Absolute paths in config that assume a different install location; ../../../ traversal in a copied template entry; a symlink inside the repo pointing to a user home directory; moving the repo so previously valid relative paths now resolve elsewhere.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- breezy: untrusted hostname
- breezy: URL must use HTTPS
- builtin: untrusted hostname
- builtin: URL must use HTTPS
- careerviet: untrusted hostname
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/00897c415f79e122.
Report an issue: GitHub.
Appendix: source
Thrown at providers/local-parser.mjs:85
return scriptArg ? expandParserArg(scriptArg, entry) : null;
}
function buildParserArgs(entry) {
const parser = entry.parser || {};
const args = [];
if (parser.script) args.push(parser.script);
if (Array.isArray(parser.args)) args.push(...parser.args);
return args.map(arg => expandParserArg(arg, entry));
}
// Resolve a configured path and confirm it stays inside the project tree.
function resolveInsideRoot(rawPath) {
const resolved = realpathSync(resolve(PROJECT_ROOT, String(rawPath)));
if (resolved !== PROJECT_ROOT && !resolved.startsWith(PROJECT_ROOT + sep)) {
throw new Error(`local-parser: path escapes the project root: ${rawPath}`);
}
return resolved;
}
// The command is either a whitelisted interpreter (resolved via PATH) or a script
// that lives inside the repo. Anything else is rejected.
function resolveCommand(command) {
const value = String(command || '');
if (!value) throw new Error('local-parser: parser.command is required');
if (!value.includes('/') && ALLOWED_INTERPRETERS.has(value)) return value;
return resolveInsideRoot(value);
}
// Validate the whole invocation and return what to spawn. Throws on anything unsafe.
function resolveInvocation(entry) {
const rawCommand = String(entry.parser?.command || '');
const command = resolveCommand(rawCommand);
const args = buildParserArgs(entry);View on GitHub (pinned to aac998c7ed)