mastra-ai/mastra · error
Invalid resourceId: ${resourceId}
Error message
Invalid resourceId: ${resourceId} What it means
savePlanToDisk writes an approved plan under <plansDir>/<resourceId>/ and defends against path traversal: it resolves the resourceId against the base plans directory and rejects any resourceId that escapes that directory (relative path containing '..' segments or an absolute path). The library throws 'Invalid resourceId: <id>' when the resourceId resolves outside the plans directory, preventing files from being written to arbitrary filesystem locations.
Source
Thrown at mastracode/sdk/src/utils/plans.ts:86
const rel = path.relative(plansDir, abs);
// Must be directly inside the plans dir (no nested subdirectories, no escaping it).
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return false;
return !rel.includes(path.sep);
}
export async function savePlanToDisk(opts: {
title: string;
plan: string;
resourceId: string;
plansDir?: string;
}): Promise<void> {
const { title, plan, resourceId } = opts;
const plansDir = opts.plansDir ?? getPlansDir();
const baseDir = path.resolve(plansDir);
const dir = path.resolve(baseDir, resourceId);
const rel = path.relative(baseDir, dir);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error(`Invalid resourceId: ${resourceId}`);
}
await fs.mkdir(dir, { recursive: true });
const now = new Date();
const timestamp = now.toISOString().replace(/:/g, '-');
const slug = slugify(title);
const filename = `${timestamp}-${slug}.md`;
const content = `# ${title}\n\nApproved: ${now.toISOString()}\n\n${plan}\n`;
await fs.writeFile(path.join(dir, filename), content, 'utf-8');
}
/**
* Read a plan markdown file by absolute path.
*
* The leading `# <title>` heading (if present) is parsed as the title and the remainingView on GitHub (pinned to 75dd419e61)
Solutions
- Sanitize the resourceId before calling: strip or reject path separators and '..' segments (e.g. resourceId.replace(/[^a-zA-Z0-9_-]/g, '_') or a slugify call).
- Validate with the same check the library uses: ensure path.relative(path.resolve(plansDir), path.resolve(plansDir, resourceId)) does not start with '..' and is not absolute.
- Pass a plain identifier (a slug or uuid) as resourceId, not a path; if you need a nested path, that is unsupported — use the default single-level layout.
- If resourceId comes from external input, validate/normalize it at the boundary (approvePlanFile caller) and throw your own descriptive error early.
Example fix
// before
await savePlanToDisk({ title, plan, resourceId: userInput }); // e.g. '../../etc/evil'
// after
const safeId = userInput.replace(/[^a-zA-Z0-9_-]/g, '_');
const rel = path.relative(path.resolve(plansDir), path.resolve(plansDir, safeId));
if (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`Invalid resourceId: ${safeId}`);
await savePlanToDisk({ title, plan, resourceId: safeId, plansDir }); Defensive patterns
Strategy: validation
Validate before calling
import path from 'node:path';
function assertSafeResourceId(resourceId, plansDir) {
const baseDir = path.resolve(plansDir);
const rel = path.relative(baseDir, path.resolve(baseDir, resourceId));
if (rel.startsWith('..') || path.isAbsolute(rel) || rel === '') {
throw new Error(`resourceId must be a single safe directory name, got: ${resourceId}`);
}
return resourceId;
} Type guard
function isSafeResourceId(resourceId, plansDir) {
const baseDir = path.resolve(plansDir);
const rel = path.relative(baseDir, path.resolve(baseDir, resourceId));
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
} Try / catch
try {
await savePlanToDisk({ title, plan, resourceId, plansDir });
} catch (err) {
if (err instanceof Error && err.message.startsWith('Invalid resourceId')) {
// sanitize resourceId and retry
await savePlanToDisk({ title, plan, resourceId: resourceId.replace(/[^a-zA-Z0-9_-]/g, '_'), plansDir });
} else throw err;
} Prevention
- Treat resourceId as an identifier, never a path; slugify or UUID it at the source.
- Never pass user/LLM-supplied strings with '/', '\\', or '..' directly as resourceId.
- Run the path.relative containment check yourself before calling approvePlanFile/savePlanToDisk.
- Log and reject unsafe ids at your API boundary with a descriptive 4xx rather than letting the library throw.
When it happens
Trigger: Calling savePlanToDisk (or approvePlanFile which delegates to it) with a resourceId that is an absolute path (e.g. '/etc/foo'), contains '..' segments (e.g. '../../escape'), or on Windows-like absolute forms, such that path.resolve(plansDir, resourceId) lands outside plansDir.
Common situations: Passing an un-sanitized resourceId derived from user or LLM input (e.g. an API key, thread id, or file path) into approvePlanFile; misusing the resourceId parameter as a target file path; concatenating paths before calling the API; resourceId strings produced from URL or CLI input that include slashes or dot-dot segments.
Related errors
- ${label} escapes workspace
- Path escapes workspace
- Invalid route path: "${path}". Path cannot contain '..', '?'
- Worker ${label} must stay within the deployed artifact root.
- ${label} must be relative
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/fceca4bd52ae76aa.
Report an issue: GitHub.