{"record":{"id":"17f84692eb1b46b7","repo":"can1357/oh-my-pi","slug":"memory-url-escapes-memory-root","errorCode":null,"errorMessage":"memory:// URL escapes memory root","messagePattern":"memory:// URL escapes memory root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/memory-protocol.ts","lineNumber":40,"sourceCode":"\tconst agentDir = getAgentDir();\n\tconst roots: string[] = [];\n\tfor (const ref of AgentRegistry.global().list()) {\n\t\tconst sm = ref.session?.sessionManager;\n\t\tif (!sm) continue;\n\t\tconst root = getMemoryRoot(agentDir, sm.getCwd());\n\t\tif (root && !roots.includes(root)) roots.push(root);\n\t}\n\treturn roots;\n}\n\nfunction memoryRootsForContext(context?: ResolveContext): string[] {\n\tif (context?.cwd) return [getMemoryRoot(getAgentDir(), context.cwd)];\n\treturn memoryRootsFromRegistry();\n}\n\nfunction ensureWithinRoot(targetPath: string, rootPath: string): void {\n\tif (targetPath !== rootPath && !targetPath.startsWith(`${rootPath}${path.sep}`)) {\n\t\tthrow new Error(\"memory:// URL escapes memory root\");\n\t}\n}\n\nfunction toMemoryValidationError(error: unknown): Error {\n\tconst message = error instanceof Error ? error.message : String(error);\n\treturn new Error(message.replace(\"skill://\", \"memory://\"));\n}\n\nexport interface MemoryGlobPattern {\n\tbaseUrl: string;\n\tglobPattern: string;\n}\n\n/**\n * Decode percent-escapes in a raw glob-suffix segment, bracket-escaping any\n * glob metacharacter that was percent-encoded so it stays a literal filename\n * character instead of becoming glob syntax.\n */","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/memory-protocol.ts#L22-L58","documentation":"memory:// URLs may only address paths inside the configured memory root directory. ensureWithinRoot checks the target (and, in tryResolveInRoot, its symlink-resolved realpath) against the root prefix and throws if the resolved path would land outside it. This is a path-traversal guard: the URL's decoded path, or a symlink inside memory, pointed outside the sanctioned root.","triggerScenarios":"A memory:// URL whose decoded path escapes via ../ segments that slip past validateRelativePath, or more commonly a symlink inside the memory root pointing to a file elsewhere on disk — the final fs.realpath check fails the prefix test and throws.","commonSituations":"Users symlinking memory files to notes elsewhere in their repo; project setups where .omp/memories contains links into shared drives; crafted memory:// URLs in agent output attempting traversal; copying a memory dir that contains stale symlinks after a move.","solutions":["Replace any symlink inside the memory root with a real copy of the target file so realpath stays under the root.","Decode the URL and confirm the path contains no ../ or absolute components; use a plain relative path under memory://root/.","Check where getMemoryRoot places the root for your session cwd and keep target files inside that directory.","If you need content from outside memory, read it directly by file path instead of through memory://."],"exampleFix":"# before — symlink escapes the memory root\nln -s ~/docs/notes.md .omp/memories/notes.md\n# after — real file inside the root\ncp ~/docs/notes.md .omp/memories/notes.md && rm .omp/memories/notes.md.orig","handlingStrategy":"validation","validationCode":"import * as path from \"node:path\";\nimport * as fs from \"node:fs/promises\";\nconst root = await fs.realpath(memoryRoot);\nconst target = path.resolve(root, decodeURIComponent(urlPath.replace(/^\\//, \"\")));\nconst real = await fs.realpath(target).catch(() => target);\nif (real !== root && !real.startsWith(root + path.sep)) {\n  throw new Error(`refusing: ${urlPath} escapes memory root`);\n}","typeGuard":"function isWithinRoot(targetPath: string, rootPath: string): boolean {\n  return targetPath === rootPath || targetPath.startsWith(`${rootPath}${path.sep}`);\n}","tryCatchPattern":"try {\n  return await handler.resolve(url, context);\n} catch (e) {\n  if (e instanceof Error && e.message === \"memory:// URL escapes memory root\") {\n    logger.warn(\"memory:// traversal blocked\", { href: url.href });\n    return null;\n  }\n  throw e;\n}","preventionTips":["Never place symlinks pointing outside the memory root directory.","Keep memory:// paths plain and relative — no ../ segments or absolute components.","After moving or copying a memory directory, audit it for stale symlinks.","Treat memory:// URLs from agent output as untrusted input and validate before reading."],"tags":["path-traversal","security","filesystem"],"backgroundTag":"path-escaping-root","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}