CherryHQ/cherry-studio · critical · Error

Agent storage directory must be a real directory: ${targetPa

Error message

Agent storage directory must be a real directory: ${targetPath}

What it means

Thrown by ensureAgentStorageDirectory as its final post-creation check: after ensuring the directory exists and asserting the path is safe, it lstat's the target and requires it to be a real directory (isDirectory true, isSymbolicLink false). This catches the race where, between ensureDir and the check, something replaced the directory with a symlink or a non-directory (TOCTOU hardening).

Source

Thrown at src/main/ai/agents/agentDataDirectory.ts:88

  const [realRoot, realTarget] = await Promise.all([
    resolveRealOrNearestExistingPath(root),
    resolveRealOrNearestExistingPath(target)
  ])
  if (realTarget !== realRoot && !isPathInside(realTarget, realRoot)) {
    throw new Error(`Agent storage path resolves outside its root: ${target}`)
  }
}

/** Ensure a Data/Agents path is a real directory contained by the Agent storage root. */
export async function ensureAgentStorageDirectory(agentsDataRoot: string, targetPath: string): Promise<void> {
  await ensureDir(asAbsolutePath(path.resolve(agentsDataRoot)))
  await assertAgentStoragePath(agentsDataRoot, targetPath)
  await ensureDir(asAbsolutePath(path.resolve(targetPath)))
  await assertAgentStoragePath(agentsDataRoot, targetPath)
  const targetStat = await lstat(asAbsolutePath(path.resolve(targetPath)))
  if (!targetStat.isDirectory || targetStat.isSymbolicLink) {
    throw new Error(`Agent storage directory must be a real directory: ${targetPath}`)
  }
}

function assertAgentId(agentId: string): void {
  if (!agentId || agentId === '.' || agentId === '..' || agentId.toLowerCase() === 'system' || /[\\/]/.test(agentId)) {
    throw new Error(`Invalid agent id for data directory: ${agentId}`)
  }
}

export function agentDataDirectoryPath(agentsDataRoot: string, agentId: string): string {
  assertAgentId(agentId)
  return path.join(agentsDataRoot, agentId)
}

async function ensureEmptyFile(filePath: string): Promise<void> {
  const existing = await lstatIfExists(filePath)
  if (existing) {
    if (!existing.isFile || existing.isSymbolicLink) {

View on GitHub (pinned to 726446b54c)

Solutions

  1. Ensure only one app instance/process manages the Data/Agents folder at a time; disable sync/backup tools that rewrite it.
  2. If a symlink exists at the target, remove it so ensureDir creates a real directory.
  3. Run on a local filesystem rather than a network/synced drive that may replace directories.
  4. Retry ensureAgentStorageDirectory once the interfering process is stopped; if it persists, inspect the path with ls -la.
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from '@main/utils/file'
const st = await lstat(path.resolve(targetPath))
if (!st.isDirectory || st.isSymbolicLink) {
  throw new Error(`Refusing: '${targetPath}' is not a real directory`)
}

Prevention

When it happens

Trigger: Immediately after ensureDir creates the directory, another process replaces it with a symlink or a file; or ensureDir resolved through a symlink leaving a link at the target path. The lstat then sees a symlink/non-directory and throws.

Common situations: Concurrent process (sync, backup, antivirus, another app instance) replacing the just-created directory; a symlinked target that ensureDir followed; filesystem oddities on network/SMB drives; a second app instance racing on the same data folder.

Related errors


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