CherryHQ/cherry-studio · error · Error
Agent memory file must be a real file: ${exact}
Error message
Agent memory file must be a real file: ${exact} What it means
resolveFileCI() resolves a filename inside the agent memory directory, preferring an exact case match. After lstat succeeds on the exact path, it verifies the entry is a regular file and NOT a symlink. This is a security invariant: agent memory files (FACT.md, JOURNAL.jsonl) must be real files to prevent symlink-based path traversal or replacement attacks.
Source
Thrown at src/main/ai/mcp/servers/agentMemory.ts:29
import type { Tool } from '@modelcontextprotocol/sdk/types.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
const logger = loggerService.withContext('McpServer:AgentMemory')
function withNoFollow(flags: number): number {
return isWin ? flags : flags | constants.O_NOFOLLOW
}
/**
* Resolve a filename within a directory using case-insensitive matching.
* Returns the full path if found (preferring exact match), or the canonical path as fallback.
*/
async function resolveFileCI(dir: string, name: string): Promise<string> {
const exact = path.join(dir, name)
try {
const fileStat = await lstat(exact)
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
throw new Error(`Agent memory file must be a real file: ${exact}`)
}
return exact
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
// exact match not found, try case-insensitive
}
try {
const entries = await readdir(dir)
const target = name.toLowerCase()
const match = entries.find((e) => e.toLowerCase() === target)
if (!match) return exact
const matchedPath = path.join(dir, match)
const fileStat = await lstat(matchedPath)
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
throw new Error(`Agent memory file must be a real file: ${matchedPath}`)
}
return matchedPathView on GitHub (pinned to 726446b54c)
Solutions
- Inspect the offending path with ls -la <agentDataPath>/memory/ and remove or replace the non-regular entry.
- If a symlink is intentional, replace it with a real file copy — symlinks are deliberately rejected here.
- Restore FACT.md / JOURNAL.jsonl from a backup as regular files.
Example fix
# before: FACT.md is a symlink ls -la memory/FACT.md # lrwxrwxrwx ... FACT.md -> /tmp/evil # after: replace with a real file rm memory/FACT.md && touch memory/FACT.md
Defensive patterns
Strategy: validation
Validate before calling
import { lstat } from 'node:fs/promises'
async function isSafeRegularFile(p: string): Promise<boolean> {
try {
const s = await lstat(p)
return s.isFile() && !s.isSymbolicLink()
} catch {
return false
}
}
// Before resolving a memory file path:
if (await isSafeRegularFile(path.join(memoryDir, 'FACT.md'))) { /* safe to proceed */ } Type guard
function isRegularFile(stat: import('node:fs').Stats): boolean {
return stat.isFile() && !stat.isSymbolicLink()
} Prevention
- Never store agent memory files as symlinks; the guards deliberately reject them.
- If you programmatically create FACT.md/JOURNAL.jsonl, use fs.writeFile (which creates regular files) rather than symlink-based indirection.
- Educate users not to place the userData directory under symlink-heavy sync tools.
When it happens
Trigger: Calling any memory tool action when a non-regular file (directory, symlink, FIFO, device node) exists at <agentDataPath>/memory/FACT.md or memory/JOURNAL.jsonl with the exact casing. lstat succeeds but isFile() is false or isSymbolicLink() is true.
Common situations: A user or another process created a symlink at FACT.md pointing elsewhere; the memory directory was corrupted by a partial filesystem restore; a directory named FACT.md was accidentally created; an editor or sync tool (Dropbox, OneDrive) replaced the file with a symlink.
Related errors
- Agent memory file must be a real file: ${matchedPath}
- Agent memory directory must be a real directory: ${memoryDir
- Agent memory file must be a real file: ${filePath}
- Agent journal must be a regular file: ${journalPath}
- Agent storage path escapes its root: ${target}
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/8e3c157f8cf8e490.
Report an issue: GitHub.