Yeachan-Heo/oh-my-codex · error · Error

session history resolved outside working directory

Error message

session history resolved outside working directory

What it means

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.

Source

Thrown at src/mcp/hermes-bridge.ts:624

    if (message.includes("artifact path") || message.includes("outside working directory")) {
      return failure("artifact_outside_safe_roots", message);
    }
    return failure("invalid_input", message);
  }
}

export async function hermesReadTail(
  args: Record<string, unknown>,
): Promise<HermesBridgeResult<{ tail: string[]; path: string }>> {
  try {
    const cwd = resolveWorkingDirectoryForState(normalizeString(args.workingDirectory, "workingDirectory"));
    const lines = normalizePositiveInteger(args.lines, DEFAULT_TAIL_LINES, MAX_TAIL_LINES);
    const path = join(cwd, ".omx", "logs", "session-history.jsonl");
    if (!existsSync(path)) return jsonResult({ tail: [], path });
    const cwdRealPath = await realpath(cwd);
    const pathRealPath = await realpath(path);
    if (!isInsideDirectory(cwdRealPath, pathRealPath)) {
      throw new Error("session history resolved outside working directory");
    }
    const info = await stat(path);
    const readBytes = Math.min(info.size, MAX_TAIL_READ_BYTES);
    const start = Math.max(0, info.size - readBytes);
    const handle = await open(pathRealPath, "r");
    try {
      const buffer = Buffer.alloc(readBytes);
      const { bytesRead } = await handle.read(buffer, 0, readBytes, start);
      const prefix = start > 0 ? "\n" : "";
      const content = `${prefix}${buffer.subarray(0, bytesRead).toString("utf-8")}`;
      return jsonResult({ tail: content.split(/\r?\n/).filter(Boolean).slice(-lines), path });
    } finally {
      await handle.close();
    }
  } catch (error) {
    return failure("invalid_input", error instanceof Error ? error.message : String(error));
  }
}

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Use the real, non-symlinked path to the working directory (fs.realpathSync it before passing it as workingDirectory)
  2. Remove symlinks for .omx/logs so history lives inside the checkout
  3. Recreate .omx/logs as a real directory inside the project

Example fix

// before
const cwd = "/tmp/proj-link";
// after
const cwd = realpathSync("/tmp/proj-link");
Defensive patterns

Strategy: validation

Validate before calling

const realCwd = realpathSync(cwd);
const realHist = realpathSync(path.join(cwd, '.omx', 'logs', 'session-history.jsonl'));
if (!realHist.startsWith(realCwd + path.sep)) throw new Error('history outside cwd');

Try / catch

try { await tailSessionHistory(cwd); } catch (e) { if ((e as Error).message.includes('outside working directory')) { /* rerun with realpathSync(cwd) */ } throw e; }

Prevention

When it happens

Trigger: 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/...

Common situations: /tmp symlinked to /private/tmp on macOS; shared .omx directory symlinked between checkouts; running inside symlinked container volumes.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/e0e5096a6c087eaf. Report an issue: GitHub.