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
- Pass the absolute workspace root via options.workspaceRoot so echoed absolute paths under that root are auto-stripped to relative.
- Use a workspace-relative path like 'src/workflows/my-workflow.workflow.ts' and never pass absolute paths.
- Strip any leading './' and trailing '/' before calling — the function handles './' but not all variants.
- 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
- Always pass options.workspaceRoot when accepting model-emitted paths so absolute paths under the root are auto-stripped.
- Validate paths with normalizeWorkspaceRelativePath in a try/catch before using them, returning a clear error to the model.
- Never concatenate user/model input directly into sandbox shell commands — always go through joinWorkspacePath.
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
- ${label} "${candidate}" escapes ${root}
- Invalid skill at ${sourceDirectory}: ${errors.join('; ')}
- Webhook URL must use HTTPS. Got: ${url.protocol}
- Webhook URL cannot target localhost
- Webhook URL cannot target private/internal IP addresses
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/05dbecdb4a399dd4.
Report an issue: GitHub.