n8n-io/n8n · error

${label} path must stay within the workspace root: ${path}.

Error message

${label} path must stay within the workspace root: ${path}. Pass a workspace-relative path like src/workflows/my-workflow.workflow.ts.

What it means

Thrown by normalizeWorkspaceRelativePath to enforce workspace containment. A path is rejected if, after stripping an allowed workspaceRoot prefix and collapsing segments, it is empty, starts with '/', starts with '~/', contains a backslash, contains a NUL byte, or has any '..' segment. This is the security guard that keeps AI-emitted file paths inside the sandbox workspace root.

Source

Thrown at packages/@n8n/instance-ai/src/workspace/workspace-paths.ts:41

	options: NormalizeWorkspaceRelativePathOptions = {},
): string {
	const label = options.resourceLabel ?? 'Workspace';
	let trimmed = path.trim().replace(/^\.\/+/, '');
	if (options.workspaceRoot && trimmed.startsWith('/')) {
		trimmed = stripWorkspaceRootPrefix(trimmed, options.workspaceRoot);
	}
	const segments = trimmed.split('/');
	const normalized = segments.filter((segment) => segment.length > 0 && segment !== '.').join('/');

	if (
		normalized.length === 0 ||
		trimmed.startsWith('/') ||
		trimmed.startsWith('~/') ||
		trimmed.includes('\\') ||
		trimmed.includes('\0') ||
		segments.some((segment) => segment === '..')
	) {
		throw new Error(
			`${label} path must stay within the workspace root: ${path}. ` +
				'Pass a workspace-relative path like src/workflows/my-workflow.workflow.ts.',
		);
	}

	return normalized;
}

export function joinWorkspacePath(root: string, path: string): string {
	const normalizedRoot = root.replace(/\/+$/, '') || '/';
	const normalizedPath = normalizeWorkspaceRelativePath(path);

	return normalizedRoot === '/' ? `/${normalizedPath}` : `${normalizedRoot}/${normalizedPath}`;
}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Pass the absolute workspace root via options.workspaceRoot so echoed absolute paths under that root are auto-stripped to relative.
  2. Use a workspace-relative path like 'src/workflows/my-workflow.workflow.ts' and never pass absolute paths.
  3. Strip any leading './' and trailing '/' before calling — the function handles './' but not all variants.
  4. Reject or sanitize model-emitted paths that contain '..' or backslashes before calling normalizeWorkspaceRelativePath.

Example fix

// before — model emitted an absolute path
const rel = normalizeWorkspaceRelativePath('/home/user/workspace/src/file.ts');
// throws

// after — pass the workspace root so the prefix is stripped
const rel = normalizeWorkspaceRelativePath(
  '/home/user/workspace/src/file.ts',
  { workspaceRoot: '/home/user/workspace' },
);
// returns 'src/file.ts'
Defensive patterns

Strategy: validation

Validate before calling

import { normalizeWorkspaceRelativePath } from './workspace/workspace-paths';

function safeRelativePath(rawPath: string, workspaceRoot?: string): string | null {
  try {
    return normalizeWorkspaceRelativePath(rawPath, { workspaceRoot });
  } catch {
    return null;
  }
}

Type guard

function isSafeWorkspacePath(path: string, workspaceRoot?: string): boolean {
  try {
    normalizeWorkspaceRelativePath(path, { workspaceRoot });
    return true;
  } catch {
    return false;
  }
}

Prevention

When it happens

Trigger: Calling normalizeWorkspaceRelativePath(path) or joinWorkspacePath(root, path) with: an absolute path like '/etc/passwd' that isn't under the configured workspaceRoot; a path containing '..' like 'src/../../secret'; a Windows path like 'src\\file'; an empty/whitespace-only string; a home-dir-relative '~/foo' path; or a path with a NUL byte injection attempt.

Common situations: An LLM tool emits an absolute sandbox path it saw in earlier shell output, but workspaceRoot was not set (or set to a different root) so the prefix strip fails; a model tries to read '/root/...' without the workspaceRoot option; a path with Windows separators is passed from a cross-platform caller; prompt injection inserts '..' to escape.

Related errors


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