Yeachan-Heo/oh-my-codex · critical · Error
session directory escapes the authorized state directory
Error message
session directory escapes the authorized state directory
What it means
The per-session directory realpath(stateDir/sessions/<session_id>) resolves outside the authorized state dir (symlinked session id component), so it is rejected before any state file is touched.
Source
Thrown at src/cli/index.ts:8157
if (
canonicalizePathForRunDirMatch(record.source_cwd) !== canonicalCwd
&& (!record.worktree_cwd || canonicalizePathForRunDirMatch(record.worktree_cwd) !== canonicalCwd)
) continue;
try {
const canonicalRunDir = realpathSync(resolve(record.run_dir));
if (!isCanonicalPathWithin(canonicalRunsRoot, canonicalRunDir)) {
throw new Error("run directory escapes the authorized runs root");
}
const stateDir = realpathSync(join(canonicalRunDir, ".omx", "state"));
if (!isCanonicalPathWithin(canonicalRunDir, stateDir)) {
throw new Error("state directory escapes the authorized run directory");
}
const session = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8")) as Record<string, unknown>;
if (session.session_id !== record.session_id) throw new Error("run session pointer changed");
const sessionDir = realpathSync(join(stateDir, "sessions", record.session_id));
if (!isCanonicalPathWithin(stateDir, sessionDir)) {
throw new Error("session directory escapes the authorized state directory");
}
candidates.push({ sessionDir, sessionId: record.session_id, record });
} catch (err) {
throw new Error(`Refusing cancellation because detached run authority is invalid: ${record.run_dir}.`, { cause: err });
}
}
if (candidates.length > 1) throw new Error("Refusing cancellation because multiple detached run authorities match.");
if (candidates.length === 0) return null;
const [{ sessionDir, sessionId, record }] = candidates;
const refs: ModeStateFileRef[] = [];
const stateFiles = await readdir(sessionDir).catch(() => [] as string[]);
for (const file of stateFiles) {
if (!isModeStateFilename(file)) continue;
const path = join(sessionDir, file);
try {
const fileStat = lstatSync(path);
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Remove the symlink under .omx/state/sessions so the session dir is a real child of state dir
- Recreate the session state by re-launching if data was only linked, not moved
- Never symlink inside .omx/state/sessions
Defensive patterns
Strategy: validation
Validate before calling
const sessionDir = realpathSync(join(stateDir, 'sessions', id));
if (!sessionDir.startsWith(realpathSync(stateDir) + sep)) throw new Error('session dir must live inside state dir'); Prevention
- Never symlink entries under sessions/
- Move state instead of linking it
- Validate session dir layout after restoring backups
When it happens
Trigger: stateDir/sessions/<session_id> is a symlink pointing elsewhere (e.g. to shared state across projects), escaping stateDir after canonicalization.
Common situations: Manually symlinking session dirs to deduplicate state, or tampered/corrupted session directories.
Related errors
- run directory escapes the authorized runs root
- state directory escapes the authorized run directory
- scale_down_cleanup_debt_path_escape:${path}
- Refusing cancellation through non-regular run state target:
- Refusing cancellation through non-regular state target ${ref
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/d2f60ae6e8e92139.
Report an issue: GitHub.