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

  1. Place the script inside the project and use a repo-relative path, e.g. parsers/my-parser.py.
  2. Remove ../ segments and any absolute path from parser.command / parser.script / detected script args.
  3. Check for symlinks: the check uses realpathSync, so retarget any in-repo symlink to a location inside the repo.
  4. 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

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


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)