CherryHQ/cherry-studio · error · Error

Agent memory directory must be a real directory: ${memoryDir

Error message

Agent memory directory must be a real directory: ${memoryDir}

What it means

assertMemoryDirectory() lstats <agentDataPath>/memory and requires it to be a real directory (isDirectory() true AND not a symlink). Symlinks are explicitly rejected to prevent a symlink swap replacing the directory with a link elsewhere. This guards every memory read and write.

Source

Thrown at src/main/ai/mcp/servers/agentMemory.ts:190

  }

  private async getAgentDataPath(): Promise<string> {
    // Deliberate existence check: memory writes must stop once the owning agent is gone.
    const agent = agentService.getAgent(this.agentId)
    if (!agent) throw new McpError(ErrorCode.InternalError, `Agent not found: ${this.agentId}`)
    const assertedPath = await assertAgentDataDirectory(path.dirname(this.agentDataPath), this.agentId)
    if (path.resolve(assertedPath) !== path.resolve(this.agentDataPath)) {
      throw new McpError(ErrorCode.InternalError, `Agent data path mismatch for ${this.agentId}`)
    }
    return assertedPath
  }

  private async assertMemoryDirectory(): Promise<string> {
    const agentDataPath = await this.getAgentDataPath()
    const memoryDir = path.join(agentDataPath, 'memory')
    const memoryStat = await lstat(memoryDir)
    if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {
      throw new Error(`Agent memory directory must be a real directory: ${memoryDir}`)
    }
    return memoryDir
  }

  private async assertRegularFileOrMissing(filePath: string): Promise<void> {
    try {
      const fileStat = await lstat(filePath)
      if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
        throw new Error(`Agent memory file must be a real file: ${filePath}`)
      }
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
    }
  }

  private async memoryUpdate(args: Record<string, string | undefined>) {
    const content = args.content
    if (!content) throw new McpError(ErrorCode.InvalidParams, "'content' is required for update action")

View on GitHub (pinned to 726446b54c)

Solutions

  1. Inspect <agentDataPath>/memory and remove the non-directory entry.
  2. Recreate it as a real directory: rm <path>/memory && mkdir <path>/memory.
  3. If the agent data directory is corrupt, recreate the agent.

Example fix

# before
ls -la agents/X/  # memory -> /tmp/evil (symlink)

# after
rm agents/X/memory
mkdir agents/X/memory
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises'

async function isSafeDirectory(p: string): Promise<boolean> {
  try {
    const s = await lstat(p)
    return s.isDirectory() && !s.isSymbolicLink()
  } catch {
    return false
  }
}

Type guard

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

Prevention

When it happens

Trigger: The memory subdirectory exists but is a symlink, a regular file, or another non-directory type. Fires on the first call to memoryUpdate, memoryAppend, or memorySearch.

Common situations: An attacker or sync tool replaced memory/ with a symlink; a file named 'memory' was created instead of a directory; filesystem corruption; a partial backup restore left memory as a file.

Related errors


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