CherryHQ/cherry-studio · critical · Error

Refusing to recursively remove unsafe agent data path: ${age

Error message

Refusing to recursively remove unsafe agent data path: ${agentDataPath}

What it means

Thrown by removeAgentDataDirectory as a hard safety guard before recursively deleting an agent data directory. It refuses to proceed when the target path is not a directory or is a symlink, preventing `rm -rf` from following a symlink out of the data root and deleting arbitrary user/system files (a classic symlink-based privilege-escalation/destruction vector).

Source

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

  }

  for (const filename of AGENT_DATA_FILES) {
    const filePath = path.join(agentDataPath, filename)
    const fileStat = await lstatIfExists(filePath)
    if (fileStat && (!fileStat.isFile || fileStat.isSymbolicLink)) {
      throw new Error(`Agent data file must be a real file: ${filePath}`)
    }
  }
  return agentDataPath
}

export async function removeAgentDataDirectory(agentsDataRoot: string, agentId: string): Promise<void> {
  const agentDataPath = agentDataDirectoryPath(agentsDataRoot, agentId)
  await assertAgentStoragePath(agentsDataRoot, agentDataPath)
  const targetStat = await lstatIfExists(agentDataPath)
  if (!targetStat) return
  if (!targetStat.isDirectory || targetStat.isSymbolicLink) {
    throw new Error(`Refusing to recursively remove unsafe agent data path: ${agentDataPath}`)
  }
  await removeDir(asAbsolutePath(agentDataPath))
}

View on GitHub (pinned to 726446b54c)

Solutions

  1. Inspect the path manually: `ls -la <agentDataPath>` and `readlink <agentDataPath>`.
  2. Do NOT bypass the guard. If you have confirmed the entry is a stray symlink/file and safe to delete, remove that specific entry directly with `rm` (not a recursive delete).
  3. Investigate how the non-directory entry got there (sync service, tampering, buggy migration) and fix the root cause.
  4. After manual cleanup, retry removeAgentDataDirectory if a real directory remains.

Example fix

// before: agentDataPath is a symlink — recursive remove is (correctly) refused
appData/agents/<id> -> /home/user/sensitive

// after: remove the symlink explicitly, never bypass with rm -rf
unlink appData/agents/<id>
# then retry the normal remove flow if needed
Defensive patterns

Strategy: type-guard

Validate before calling

import { lstat } from 'node:fs/promises'
async function canSafeRemove(p: string): Promise<boolean> {
  try {
    const s = await lstat(p)
    return s.isDirectory() && !s.isSymbolicLink()
  } catch {
    return true // nothing to remove
  }
}
if (!(await canSafeRemove(agentDataPath))) {
  throw new Error(`Refusing to remove non-directory/symlink: ${agentDataPath}`)
}

Type guard

function isSafeRemovableDirectory(stat: import('node:fs').Stats): boolean {
  return stat.isDirectory() && !stat.isSymbolicLink()
}

Try / catch

try {
  await removeAgentDataDirectory(root, agentId)
} catch (e) {
  if (e instanceof Error && /unsafe agent data path/.test(e.message)) {
    // halt automated cleanup; require manual inspection — never bypass with rm -rf
    logger.error('Unsafe agent data path; manual cleanup required', { agentDataPath, error: e })
  } else throw e
}

Prevention

When it happens

Trigger: removeAgentDataDirectory(agentsDataRoot, agentId) is called (agent deletion / cleanup) and lstat of the agent data path reports a non-directory entry or a symbolic link.

Common situations: An attacker or sync service replaced the agent dir with a symlink to a sensitive location; the data path was manually turned into a file; attempting to clean up after a corrupted/tampered data store.

Related errors


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