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 remaining

View on GitHub (pinned to 75dd419e61)

Solutions

  1. 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).
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/fceca4bd52ae76aa. Report an issue: GitHub.