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 limitView on GitHub (pinned to 726446b54c)
Solutions
- Pass the full path to a regular file, removing any trailing slash or directory-only segment.
- If the path is a symlink, resolve its target with fs.realpath and confirm it is a regular file before calling read.
- 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
- Build read paths from a verified file listing (fs.readdir) rather than user free text.
- Strip trailing slashes and reject directory-shaped paths before calling read.
- Resolve symlinks with fs.realpath and re-check isFile() on the target.
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
- File not found: ${filePath}
- Cannot read binary file: ${filePath}
- Invalid offset: ${offset + 1}. File has ${lines.length} line
- Invalid arguments for write: ${parsed.error}
- Access denied: Path is outside the configured workspace root
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/4e0fd22685f73e02.
Report an issue: GitHub.