CherryHQ/cherry-studio · error · Error

File not found: ${displayPath}

Error message

File not found: ${displayPath}

What it means

Thrown by readCanonicalLocalFile() when lstat(target) fails with ENOENT or ENOTDIR on a path that was already confirmed to exist by a prior realpath call. This indicates a TOCTOU (time-of-check-to-time-of-use) race: the file existed when realpath resolved it but was deleted or replaced before lstat ran. The error is intentionally generic ('File not found') rather than exposing the race, so callers treat it as a missing-file condition.

Source

Thrown at src/main/ai/channels/security/localFileResolver.ts:36

}

/**
 * Read an already-canonical path into a `FileAttachment`. Performs NO path
 * authorization — the caller owns containment (see `resolveWorkspaceFile`).
 */
export async function readCanonicalLocalFile(
  requestedPath: string,
  canonicalPath: string,
  displayPath: string
): Promise<FileAttachment> {
  const target = AbsoluteFilePathSchema.parse(canonicalPath)

  let stats: Awaited<ReturnType<typeof lstat>>
  try {
    stats = await lstat(target)
  } catch (error) {
    if (isErrnoException(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
      throw new Error(`File not found: ${displayPath}`)
    }
    throw error
  }
  if (!stats.isFile) {
    throw new Error(`Not a regular file: ${displayPath}`)
  }

  // The snapshot pins the inode and fixes the read length at open time, so the size
  // check and the read see the same file even if the path is replaced meanwhile.
  const snapshot = await openReadableFileSnapshot(target)
  try {
    if (snapshot.size > MAX_FILE_SIZE_BYTES) {
      throw new Error(`File exceeds the ${MAX_FILE_SIZE_BYTES} byte limit (${snapshot.size} bytes): ${displayPath}`)
    }

    const data = await readStreamToBuffer(snapshot.createReadStream())
    // Re-check against the actual read size: the file can grow between stat and read.
    if (data.length > MAX_FILE_SIZE_BYTES) {

View on GitHub (pinned to 726446b54c)

Solutions

  1. Retry the resolution from the top-level caller (resolveWorkspaceFile or resolveLocalFile) — if the file was transiently gone, it may reappear.
  2. If the deletion is expected (e.g., a cleanup step), coordinate timing so reads happen before deletes.
  3. On network filesystems, add a short retry with exponential backoff for ENOENT on recently-listed files.
Defensive patterns

Strategy: retry

Validate before calling

import { exists } from '@main/utils/file'

// Pre-check existence before calling resolveLocalFile or resolveWorkspaceFile
if (!(await exists(requestedPath))) {
  return null // file is gone, caller can handle gracefully
}

Try / catch

try {
  return await readCanonicalLocalFile(requestedPath, canonicalPath, displayPath)
} catch (error) {
  if (error instanceof Error && error.message.startsWith('File not found:')) {
    // TOCTOU race — file existed at realpath time but gone by lstat time
    logger.warn('File disappeared during resolution (TOCTOU)', { displayPath })
    return null
  }
  throw error
}

Prevention

When it happens

Trigger: Called after realpath succeeds in either resolveWorkspaceFile() or resolveLocalFile(). The file is deleted between the realpath call and the lstat call inside readCanonicalLocalFile. Common in concurrent environments where multiple agents or processes modify the same workspace simultaneously.

Common situations: A concurrent agent step or external process deleted the file between the path resolution and the stat; a file-watcher or build tool cleaned the file; the workspace is on a network filesystem with eventual-consistency delays.

Related errors


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