CherryHQ/cherry-studio · error · Error

Path is not a file: ${filePath}

Error message

Path is not a file: ${filePath}

What it means

Thrown by the read tool after fs.stat succeeds but stats.isFile() is false. The path resolved, passed workspace-root validation, and exists on disk, but it is a directory, FIFO, socket, device, or symlink-to-directory rather than a regular file. The read handler can only stream text content from regular files, so any non-regular inode is rejected here before the binary check or readFile is attempted.

Source

Thrown at src/main/ai/mcp/servers/filesystem/tools/read.ts:44

- Empty files return a warning`,
  inputSchema: z.toJSONSchema(ReadToolSchema)
}

// Handler implementation
export async function handleReadTool(args: unknown, baseDir: string) {
  const parsed = ReadToolSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for read: ${parsed.error}`)
  }

  const filePath = parsed.data.file_path
  const validPath = await validatePath(filePath, baseDir)

  // Check if file exists
  try {
    const stats = await fs.stat(validPath)
    if (!stats.isFile()) {
      throw new Error(`Path is not a file: ${filePath}`)
    }
  } catch (error: any) {
    if (error.code === 'ENOENT') {
      throw new Error(`File not found: ${filePath}`)
    }
    throw error
  }

  // Check if file is binary
  if (await isBinaryFile(validPath)) {
    throw new Error(`Cannot read binary file: ${filePath}`)
  }

  // Read file content
  const content = await fs.readFile(validPath, 'utf-8')
  const lines = content.split('\n')

  // Apply offset and limit

View on GitHub (pinned to 726446b54c)

Solutions

  1. Pass the full path to a regular file, removing any trailing slash or directory-only segment.
  2. If the path is a symlink, resolve its target with fs.realpath and confirm it is a regular file before calling read.
  3. When the path source is untrusted (user input, UI selection), pre-check with fs.stat + stats.isFile() before invoking the tool.

Example fix

// before
await handleReadTool({ file_path: '/proj/src' }, baseDir) // throws: Path is not a file

// after
await handleReadTool({ file_path: '/proj/src/index.ts' }, baseDir)
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs/promises'
async function assertReadableFile(absPath: string): Promise<void> {
  const stats = await fs.stat(absPath) // throws ENOENT if missing
  if (!stats.isFile()) {
    throw new Error(`Refusing to read non-file path: ${absPath}`)
  }
}
// call before handleReadTool

Type guard

function isRegularFile(stats: fs.Stats): boolean {
  return stats.isFile()
}

Try / catch

try {
  await handleReadTool(args, baseDir)
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Path is not a file')) {
    // surface 'expected a regular file' to the caller
  } else throw e
}

Prevention

When it happens

Trigger: Calling handleReadTool with file_path that resolves to a directory (e.g. '/proj/src' or '/proj/src/'), a named pipe/special device file inside the workspace, or a symlink whose realpath target is a directory. The ENOENT branch is skipped because stat succeeded; only isFile() is false.

Common situations: Path built by joining a folder with a missing filename; caller appends a trailing slash; user selects a folder node in a file tree UI and triggers read; a symlink inside the workspace points at a directory elsewhere.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/4e0fd22685f73e02. Report an issue: GitHub.