{"record":{"id":"e0e5096a6c087eaf","repo":"Yeachan-Heo/oh-my-codex","slug":"session-history-resolved-outside-working-directory","errorCode":null,"errorMessage":"session history resolved outside working directory","messagePattern":"session history resolved outside working directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mcp/hermes-bridge.ts","lineNumber":624,"sourceCode":"    if (message.includes(\"artifact path\") || message.includes(\"outside working directory\")) {\n      return failure(\"artifact_outside_safe_roots\", message);\n    }\n    return failure(\"invalid_input\", message);\n  }\n}\n\nexport async function hermesReadTail(\n  args: Record<string, unknown>,\n): Promise<HermesBridgeResult<{ tail: string[]; path: string }>> {\n  try {\n    const cwd = resolveWorkingDirectoryForState(normalizeString(args.workingDirectory, \"workingDirectory\"));\n    const lines = normalizePositiveInteger(args.lines, DEFAULT_TAIL_LINES, MAX_TAIL_LINES);\n    const path = join(cwd, \".omx\", \"logs\", \"session-history.jsonl\");\n    if (!existsSync(path)) return jsonResult({ tail: [], path });\n    const cwdRealPath = await realpath(cwd);\n    const pathRealPath = await realpath(path);\n    if (!isInsideDirectory(cwdRealPath, pathRealPath)) {\n      throw new Error(\"session history resolved outside working directory\");\n    }\n    const info = await stat(path);\n    const readBytes = Math.min(info.size, MAX_TAIL_READ_BYTES);\n    const start = Math.max(0, info.size - readBytes);\n    const handle = await open(pathRealPath, \"r\");\n    try {\n      const buffer = Buffer.alloc(readBytes);\n      const { bytesRead } = await handle.read(buffer, 0, readBytes, start);\n      const prefix = start > 0 ? \"\\n\" : \"\";\n      const content = `${prefix}${buffer.subarray(0, bytesRead).toString(\"utf-8\")}`;\n      return jsonResult({ tail: content.split(/\\r?\\n/).filter(Boolean).slice(-lines), path });\n    } finally {\n      await handle.close();\n    }\n  } catch (error) {\n    return failure(\"invalid_input\", error instanceof Error ? error.message : String(error));\n  }\n}","sourceCodeStart":606,"sourceCodeEnd":642,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/mcp/hermes-bridge.ts#L606-L642","documentation":"Thrown when the session-history file (.omx/logs/session-history.jsonl) resolves, after realpath, to a location outside the real working directory. Like the artifact checks, this prevents reading history through symlinked paths that escape the sandbox.","triggerScenarios":"cwd contains a symlinked .omx or .omx/logs directory pointing elsewhere; cwd itself is a symlink so realpath(cwd) differs from the path used to join .omx/logs/...","commonSituations":"/tmp symlinked to /private/tmp on macOS; shared .omx directory symlinked between checkouts; running inside symlinked container volumes.","solutions":["Use the real, non-symlinked path to the working directory (fs.realpathSync it before passing it as workingDirectory)","Remove symlinks for .omx/logs so history lives inside the checkout","Recreate .omx/logs as a real directory inside the project"],"exampleFix":"// before\nconst cwd = \"/tmp/proj-link\";\n// after\nconst cwd = realpathSync(\"/tmp/proj-link\");","handlingStrategy":"validation","validationCode":"const realCwd = realpathSync(cwd);\nconst realHist = realpathSync(path.join(cwd, '.omx', 'logs', 'session-history.jsonl'));\nif (!realHist.startsWith(realCwd + path.sep)) throw new Error('history outside cwd');","typeGuard":null,"tryCatchPattern":"try { await tailSessionHistory(cwd); } catch (e) { if ((e as Error).message.includes('outside working directory')) { /* rerun with realpathSync(cwd) */ } throw e; }","preventionTips":["Pass realpathSync(cwd) as workingDirectory, especially on macOS /tmp","Never symlink .omx or .omx/logs outside the checkout"],"tags":["symlink","security","session-history","mcp"],"backgroundTag":"symlink-escapes-sandbox","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}