mastra-ai/mastra · warning · HTTPException
skillPath is not a directory: ${resolvedPath}
Error message
skillPath is not a directory: ${resolvedPath} What it means
A 400 thrown after the traversal check: `fs.stat(resolvedPath)` succeeded but `stat.isDirectory()` is false, so `skillPath` points at a file (often a SKILL.md itself) rather than a skill directory. The publisher expects a directory containing SKILL.md.
Source
Thrown at packages/server/src/server/handlers/stored-skills.ts:625
});
// Validate skillPath to prevent path traversal
const path = await import('node:path');
const fs = await import('node:fs/promises');
const resolvedPath = path.default.resolve(skillPath);
const allowedBase = path.default.resolve(process.env.SKILLS_BASE_DIR || process.cwd());
if (!resolvedPath.startsWith(allowedBase + path.default.sep) && resolvedPath !== allowedBase) {
throw new HTTPException(400, {
message: `skillPath must be within the allowed directory: ${allowedBase}`,
});
}
// Verify the source directory exists and contains a SKILL.md before attempting
// to publish, so callers get a 400 with context instead of a raw 500/ENOENT.
try {
const stat = await fs.stat(resolvedPath);
if (!stat.isDirectory()) {
throw new HTTPException(400, { message: `skillPath is not a directory: ${resolvedPath}` });
}
} catch (err) {
if (err instanceof HTTPException) throw err;
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
throw new HTTPException(400, {
message: `skillPath does not exist on the server filesystem: ${resolvedPath}. Create the skill directory (with a SKILL.md) before publishing, or use a skill that was materialized to disk.`,
});
}
throw err;
}
try {
await fs.stat(path.default.join(resolvedPath, 'SKILL.md'));
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
throw new HTTPException(400, {
message: `skillPath is missing SKILL.md: ${resolvedPath}`,
});
}View on GitHub (pinned to 75dd419e61)
Solutions
- Pass the skill's directory path, not the SKILL.md file path
- Strip a trailing `/SKILL.md` from the path before calling
- Verify locally: `(await fs.stat(p)).isDirectory()` before publishing
Example fix
// before
await publishSkill({ skillPath: '/srv/skills/my-skill/SKILL.md' });
// after
await publishSkill({ skillPath: '/srv/skills/my-skill' }); Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs/promises';
const stat = await fs.stat(skillPath);
if (!stat.isDirectory()) throw new Error('skillPath must be the skill directory, not SKILL.md or another file'); Try / catch
try { await publishStoredSkill({ skillPath }); } catch (e) {
if (e.status === 400 && /is not a directory/.test(e.message)) throw new Error('Pass the directory containing SKILL.md, not a file path');
throw e;
} Prevention
- Pass the folder path, never SKILL.md itself
- Stat the path and check isDirectory() before calling the API
- Avoid trailing-path joins that append SKILL.md twice
- Use directory pickers (not file pickers) in tooling that fills skillPath
When it happens
Trigger: Passing `/path/to/my-skill/SKILL.md` or any regular file as `skillPath` in the publish request.
Common situations: Copying the file path instead of the folder path from an editor; shell autocomplete picking the file inside the directory; scripts joining the path with 'SKILL.md' twice.
Related errors
- bad request: ${responseText}
- Missing required query param: ${label}
- ${label} must be relative
- ${label} escapes workspace
- Path escapes workspace
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/220e545ad1c99137.
Report an issue: GitHub.