{"record":{"id":"9ba0a76f9811c806","repo":"CherryHQ/cherry-studio","slug":"path-is-not-a-file-filepath","errorCode":null,"errorMessage":"Path is not a file: ${filePath}","messagePattern":"Path is not a file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/main/ai/mcp/servers/filesystem/tools/edit.ts","lineNumber":48,"sourceCode":"}\n\n// Handler implementation\nexport async function handleEditTool(args: unknown, baseDir: string) {\n  const parsed = EditToolSchema.safeParse(args)\n  if (!parsed.success) {\n    throw new Error(`Invalid arguments for edit: ${parsed.error}`)\n  }\n\n  const { file_path: filePath, old_string: oldString, new_string: newString, replace_all: replaceAll } = parsed.data\n\n  // Validate path\n  const validPath = await validatePath(filePath, baseDir)\n\n  // Check if file exists\n  try {\n    const stats = await fs.stat(validPath)\n    if (!stats.isFile()) {\n      throw new Error(`Path is not a file: ${filePath}`)\n    }\n  } catch (error: any) {\n    if (error.code === 'ENOENT') {\n      // If old_string is empty, this is a create new file operation\n      if (oldString === '') {\n        // Create parent directory if needed\n        const parentDir = path.dirname(validPath)\n        await fs.mkdir(parentDir, { recursive: true })\n\n        // Write the new content\n        await fs.writeFile(validPath, newString, 'utf-8')\n\n        logger.info('File created', { path: validPath })\n\n        const relativePath = path.relative(baseDir, validPath)\n        return {\n          content: [\n            {","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/filesystem/tools/edit.ts#L30-L66","documentation":"fs.stat on the validated edit target succeeded but stats.isFile() returned false — the path resolves to a directory (or other non-file type) but the edit tool requires a regular file. The user-facing filePath appears in the message. This is a semantic mismatch, not a missing file (ENOENT is handled separately at edit.ts:51).","triggerScenarios":"The caller passed a directory path to the edit tool, expecting it to edit 'all files inside' or treating a directory like a file. Also fires for special files (device, socket) that are not regular files.","commonSituations":"A model confusing a directory path (e.g. '/src/components') with a file path ('/src/components/Button.tsx'); passing a path that points at a symlink to a directory.","solutions":["Confirm the path points to a regular file — use the ls or read tool first.","If the intent is to edit multiple files, call edit once per file with a concrete file path.","Strip any trailing slash from the path before sending, since directories are often written that way."],"exampleFix":"// before\nif (!stats.isFile()) {\n  throw new Error(`Path is not a file: ${filePath}`)\n}\n\n// after — say what it actually is\nconst kind = stats.isDirectory() ? 'directory' : stats.isSymbolicLink() ? 'symlink' : 'non-file'\nthrow new Error(`Path is a ${kind}, not a file: ${filePath}`)","handlingStrategy":"validation","validationCode":"// Confirm the target is a regular file before editing.\nimport { stat } from 'fs/promises'\nasync function assertIsFile(p: string): Promise<void> {\n  const s = await stat(p)\n  if (!s.isFile()) throw new Error(`Path is not a file: ${p}`)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Call read or ls first to confirm the path is a file, not a directory.","Strip trailing slashes from paths before sending.","Edit one file per call; do not pass a directory path expecting bulk edits.","Resolve symlinks before checking type if symlinks are present in the workspace."],"tags":["filesystem","edit","mcp-tool","type-mismatch"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}