{"record":{"id":"0f0536ac496965f0","repo":"CherryHQ/cherry-studio","slug":"agent-memory-directory-must-be-a-real-directory-0f0536","errorCode":null,"errorMessage":"Agent memory directory must be a real directory: ${memoryDir}","messagePattern":"Agent memory directory must be a real directory: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/agentMemory.ts","lineNumber":190,"sourceCode":"  }\n\n  private async getAgentDataPath(): Promise<string> {\n    // Deliberate existence check: memory writes must stop once the owning agent is gone.\n    const agent = agentService.getAgent(this.agentId)\n    if (!agent) throw new McpError(ErrorCode.InternalError, `Agent not found: ${this.agentId}`)\n    const assertedPath = await assertAgentDataDirectory(path.dirname(this.agentDataPath), this.agentId)\n    if (path.resolve(assertedPath) !== path.resolve(this.agentDataPath)) {\n      throw new McpError(ErrorCode.InternalError, `Agent data path mismatch for ${this.agentId}`)\n    }\n    return assertedPath\n  }\n\n  private async assertMemoryDirectory(): Promise<string> {\n    const agentDataPath = await this.getAgentDataPath()\n    const memoryDir = path.join(agentDataPath, 'memory')\n    const memoryStat = await lstat(memoryDir)\n    if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {\n      throw new Error(`Agent memory directory must be a real directory: ${memoryDir}`)\n    }\n    return memoryDir\n  }\n\n  private async assertRegularFileOrMissing(filePath: string): Promise<void> {\n    try {\n      const fileStat = await lstat(filePath)\n      if (!fileStat.isFile() || fileStat.isSymbolicLink()) {\n        throw new Error(`Agent memory file must be a real file: ${filePath}`)\n      }\n    } catch (error) {\n      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error\n    }\n  }\n\n  private async memoryUpdate(args: Record<string, string | undefined>) {\n    const content = args.content\n    if (!content) throw new McpError(ErrorCode.InvalidParams, \"'content' is required for update action\")","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/agentMemory.ts#L172-L208","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect <agentDataPath>/memory and remove the non-directory entry.","Recreate it as a real directory: rm <path>/memory && mkdir <path>/memory.","If the agent data directory is corrupt, recreate the agent."],"exampleFix":"# before\nls -la agents/X/  # memory -> /tmp/evil (symlink)\n\n# after\nrm agents/X/memory\nmkdir agents/X/memory","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises'\n\nasync function isSafeDirectory(p: string): Promise<boolean> {\n  try {\n    const s = await lstat(p)\n    return s.isDirectory() && !s.isSymbolicLink()\n  } catch {\n    return false\n  }\n}","typeGuard":"function isRealDirectory(stat: import('node:fs').Stats): boolean {\n  return stat.isDirectory() && !stat.isSymbolicLink()\n}","tryCatchPattern":null,"preventionTips":["Always create the memory directory with fs.mkdir, never via symlink.","Do not let sync tools convert directories under userData into symlinks.","Run periodic integrity checks on agent data directories in CI."],"tags":["security","filesystem","symlink-guard","agent-memory"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}