mastra-ai/mastra · error

Path is a directory

Error message

Path is a directory

What it means

readWorkspaceFile reads a text file from a workspace; after resolving and confining the path it lstats the target and refuses to read directories. This error is thrown when the requested path exists inside the workspace but is a directory rather than a regular file, because reading it as file content is meaningless. The caller should use a directory-listing endpoint instead.

Source

Thrown at mastracode/factory/src/routes/fs.ts:371

  return {
    workspacePath: workspace,
    root: safeRoot,
    rootPath: confinedRootPath,
    entries: await listRenderedEntries(confinedRootPath),
  };
}

export async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {
  const safePath = assertRelativePath(path, 'path');
  const relativeRoot = safePath.split('/')[0] ?? '';
  assertApprovedRenderedRoot(relativeRoot);
  const {
    workspace,
    path: confinedPath,
    relativePath,
  } = await confinedWorkspaceRelativePath(root, workspacePath, path);
  const info = await lstat(confinedPath);
  if (info.isDirectory()) throw new Error('Path is a directory');
  if (!info.isFile()) throw new Error('Unsupported file type');

  const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);
  const contentBuffer = Buffer.alloc(bytesToRead);
  const handle = await open(confinedPath, 'r');
  try {
    await handle.read(contentBuffer, 0, bytesToRead, 0);
  } finally {
    await handle.close();
  }

  try {
    const content = TEXT_DECODER.decode(contentBuffer);
    return {
      workspacePath: workspace,
      path: relativePath,
      name: relativePath.split('/').pop() ?? relativePath,
      size: info.size,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Check that the path targets a file, not a directory (ls or stat the path inside the workspace).
  2. If you want the directory contents, use the directory-listing / rendered-listing endpoint for that workspace instead.
  3. Fix client code to branch on the entry type from listing results before calling the read endpoint.
  4. If you expected a file, verify the file wasn't replaced by a directory of the same name during a build or checkout.

Example fix

// before: reading a directory
GET /fs/file?path=.artifacts            // → 'Path is a directory'
// after: read a file inside it
GET /fs/file?path=.artifacts/report.txt
Defensive patterns

Strategy: validation

Validate before calling

import { stat } from 'node:fs/promises';
async function isFileInWorkspace(workspace: string, rel: string): Promise<boolean> {
  try {
    const info = await stat(resolve(workspace, rel));
    return info.isFile();
  } catch {
    return false;
  }
}
// only call readWorkspaceFile when this returns true; else use the listing endpoint

Type guard

function isFileEntry(entry: { type: string }): entry is { type: 'file' } {
  return entry.type === 'file';
}

Try / catch

try {
  const file = await readWorkspaceFile(root, ws, path);
} catch (e) {
  if (e instanceof Error && e.message === 'Path is a directory') {
    return listWorkspaceRenderedPath(root, ws, path); // fall back to listing
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling readWorkspaceFile (or the fs read route) with a path query that names an existing directory inside the workspace, e.g. path=src or path=.artifacts.

Common situations: Passing a folder path copied from a listing response instead of an entry file path; assuming the artifacts root itself is readable as a file; UI bugs that don't distinguish entry.type === 'directory' before opening a reader.

Related errors


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