n8n-io/n8n · error · Error

${label} "${candidate}" escapes ${root}

Error message

${label} "${candidate}" escapes ${root}

What it means

Thrown by `resolveInside()` in computer-use/runner.ts when joining a scenario-declared path (fixture path or sandbox destination) onto a root directory would produce a resolved path outside that root. The check uses `node:path.resolve` then `isContained(root, full)`; the root itself is allowed (empty candidate is a no-op), but any `..` segment or absolute path that escapes the root is rejected. Purpose: keep scenario JSON authors honest so a malicious or careless fixture path can't read or write outside the sandbox.

Source

Thrown at packages/@n8n/instance-ai/evaluations/computer-use/runner.ts:186

function resolveFixture(fixturesDir: string, fixturePath: string): string {
	return resolveInside(fixturesDir, fixturePath, 'fixture path');
}

/**
 * Join `candidate` onto `root` and assert the result stays within `root`.
 * Throws if the resolved path escapes (e.g. via `..`). Used to keep scenario
 * authors honest when declaring fixture paths and sandbox destinations.
 *
 * Exported for unit testing — keep the import surface narrow.
 */
export function resolveInside(root: string, candidate: string, label: string): string {
	const rootResolved = resolve(root);
	const fullResolved = resolve(rootResolved, candidate);
	// Allow the root itself (e.g. empty candidate) as a no-op destination;
	// otherwise require strict containment.
	if (fullResolved !== rootResolved && !isContained(rootResolved, fullResolved)) {
		throw new Error(`${label} "${candidate}" escapes ${root}`);
	}
	return fullResolved;
}

// ---------------------------------------------------------------------------
// Optional pre-seeded workflow (for scenarios that say "look at my workflow X")
// ---------------------------------------------------------------------------

async function maybeSeedWorkflow(
	client: N8nClient,
	scenario: Scenario,
	fixturesDir: string,
	logger: EvalLogger,
): Promise<void> {
	const path = scenario.setup?.seedWorkflow;
	if (!path) return;

	const fixturePath = resolveFixture(fixturesDir, path);

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Open the scenario JSON cited by the run and find the field matching the `label` in the message ('fixture path' → check `seedWorkflow`/fixture refs; 'sandbox path' → check `setup.seeds[].to`).
  2. Replace the escaping path with one that resolves strictly under the root: use a relative path with no `..`, or copy the external file into the fixtures dir first.
  3. If you genuinely need a path outside the root, that's unsupported by design — restructure so the file lives under `fixtures/` or `sandboxDir`.

Example fix

// before (scenario.json)
{ "setup": { "seeds": [{ "from": "data.json", "to": "../../shared/out.json" }] } }
// after
{ "setup": { "seeds": [{ "from": "data.json", "to": "shared/out.json" }] } }
Defensive patterns

Strategy: type-guard

Validate before calling

import { resolve, relative } from 'node:path';
function isContained(root: string, child: string): boolean {
  const rel = relative(root, child);
  return rel === '' || (!rel.startsWith('..') && !resolve(child).isAbsolute);
}
function safeResolveInside(root: string, candidate: string): string | null {
  const r = resolve(root);
  const f = resolve(r, candidate);
  return f === r || isContained(r, f) ? f : null;
}

Type guard

function isSafePath(root: string, candidate: string): boolean {
  const r = resolve(root);
  const f = resolve(r, candidate);
  if (f === r) return true;
  const rel = relative(r, f);
  return rel.length > 0 && !rel.startsWith('..') && !path.isAbsolute(rel);
}

Prevention

When it happens

Trigger: A scenario JSON declares `seedWorkflow: '../../../etc/passwd'` or a fixture path like `'/etc/secrets.json'` (absolute, escapes). A sandbox `to: '../../../../tmp'` declared in a seed entry. Any fixture/destination resolved relative to `fixturesDir` or `sandboxDir` that does not stay underneath it. Called from `resolveFixture` (label 'fixture path') and from the seed loop (label 'sandbox path'), so the `label` in the message tells you which.

Common situations: Scenario author uses an absolute path by mistake. A copy-pasted fixture path from a different scenario layout where the relative root differs. A legitimate intent to share a fixture from outside the fixtures dir — the model forbids this; you must put the fixture inside the fixtures dir or symlink/copy it in.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/f175ad8bfa5f0886. Report an issue: GitHub.