CherryHQ/cherry-studio · critical · Error

Agent storage path contains a symbolic link: ${current}

Error message

Agent storage path contains a symbolic link: ${current}

What it means

Thrown by assertAgentStoragePath while walking each segment of the relative path from root to target: if any intermediate path component is a symbolic link (detected via lstat, which does not follow it), the walk aborts. This prevents a symlink planted inside the managed tree from redirecting a write outside the root (a classic TOCTOU/traversal escalation).

Source

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

  const root = asAbsolutePath(path.resolve(agentsDataRoot))
  const target = asAbsolutePath(path.resolve(targetPath))
  if (target !== root && !isPathInside(target, root)) {
    throw new Error(`Agent storage path escapes its root: ${target}`)
  }

  const rootStat = await lstatIfExists(root)
  if (!rootStat?.isDirectory || rootStat.isSymbolicLink) {
    throw new Error(`Agent storage root must be a real directory: ${root}`)
  }

  let current = root
  const relative = path.relative(root, target)
  for (const segment of relative ? relative.split(path.sep) : []) {
    current = asAbsolutePath(path.join(current, segment))
    const currentStat = await lstatIfExists(current)
    if (!currentStat) break
    if (currentStat.isSymbolicLink) {
      throw new Error(`Agent storage path contains a symbolic link: ${current}`)
    }
    if (current !== target && !currentStat.isDirectory) {
      throw new Error(`Agent storage path parent is not a directory: ${current}`)
    }
  }

  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)))

View on GitHub (pinned to 726446b54c)

Solutions

  1. Remove the offending symlink and replace it with a real directory/file as appropriate.
  2. Investigate how the symlink got there — a recurring cause indicates a process (sync, backup, agent logic) recreating it.
  3. Do not weaken the check to follow symlinks; the guard exists to prevent traversal escalation.
  4. Run a filesystem audit for symlinks under Data/Agents: find <agentsDataRoot> -type l.
Defensive patterns

Strategy: validation

Validate before calling

import { lstatIfExists } from './agentDataDirectory'
// Walk root..target and reject any symlink segment
for (const seg of path.relative(root, target).split(path.sep)) {
  current = path.join(current, seg)
  const st = await lstatIfExists(current)
  if (st?.isSymbolicLink) throw new Error(`Refusing: '${current}' is a symlink inside agents root`)
}

Prevention

When it happens

Trigger: A symlink exists anywhere along the path between Data/Agents and the target (e.g. Data/Agents/<agentId>/memory is a symlink to /tmp). A compromised or misconfigured agent data folder contains a link; a backup restore introduced links; an external tool created a symlinked subdirectory.

Common situations: User or another process created a symlink inside the agent data tree; a migration/sync tool preserved links; a malicious or buggy agent wrote a symlink; cross-platform sync (e.g. cloud drive) materialized links.

Related errors


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