{"record":{"id":"f293d80097376a20","repo":"Yeachan-Heo/oh-my-codex","slug":"unsafe-autopilot-context-directory-resolved-path","errorCode":null,"errorMessage":"Unsafe Autopilot context directory: resolved path escapes repository root","messagePattern":"Unsafe Autopilot context directory: resolved path escapes repository root","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/hooks/keyword-detector.ts","lineNumber":377,"sourceCode":"\nasync function ensureSafeAutopilotContextDir(sourceCwd: string): Promise<string> {\n  const rootRealPath = await realpath(sourceCwd);\n  const omxDir = join(sourceCwd, '.omx');\n  await mkdir(omxDir, { recursive: true });\n  if ((await lstat(omxDir)).isSymbolicLink()) {\n    throw new Error('Unsafe Autopilot context directory: .omx is a symbolic link');\n  }\n\n  const contextDir = join(omxDir, 'context');\n  await mkdir(contextDir, { recursive: true });\n  if ((await lstat(contextDir)).isSymbolicLink()) {\n    throw new Error('Unsafe Autopilot context directory: .omx/context is a symbolic link');\n  }\n\n  const contextRealPath = await realpath(contextDir);\n  const relativeToRoot = relative(rootRealPath, contextRealPath);\n  if (relativeToRoot === '' || relativeToRoot.startsWith('..') || isAbsolute(relativeToRoot)) {\n    throw new Error('Unsafe Autopilot context directory: resolved path escapes repository root');\n  }\n  return contextDir;\n}\n\nasync function writeUniqueAutopilotContextSnapshot(\n  sourceCwd: string,\n  slug: string,\n  nowIso: string,\n  body: string,\n): Promise<string> {\n  const contextDir = await ensureSafeAutopilotContextDir(sourceCwd);\n  const timestamp = utcCompactTimestamp(nowIso);\n  for (let attempt = 0; attempt < 100; attempt += 1) {\n    const suffix = attempt === 0 ? '' : `-${attempt + 1}`;\n    const filename = `${slug}-${timestamp}${suffix}.md`;\n    const relativePath = `.omx/context/${filename}`;\n    const absolutePath = resolve(contextDir, filename);\n    try {","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/hooks/keyword-detector.ts#L359-L395","documentation":"Thrown by ensureSafeAutopilotContextDir when the realpath of .omx/context resolves outside the repository root (relative path from root is '', starts with '..', or is absolute — the latter indicating a different filesystem root). Even after passing the symlink checks, the resolved real path must remain within the repo.","triggerScenarios":".omx or .omx/context being a bind mount (Linux), a mount point, or residing in a repo whose realpath differs from sourceCwd (e.g. sourceCwd itself is a symlink to the actual checkout, making relative(rootRealPath, contextRealPath) escape); context dir on a different drive/root on Windows yielding an absolute relative() result.","commonSituations":"Checking out the repo through a symlinked path (ln -s ~/work/myrepo ~/link && cd ~/link); Docker bind mounts mapping .omx to a host path; Windows subst/junction drives; CI systems that materialize workspaces via links so realpath lands outside the nominal root.","solutions":["Run the hook from the real repository path (resolve symlinks in your cwd: cd $(realpath .)) and avoid symlinked checkouts","Remove bind mounts/junctions on .omx or .omx/context so the directory physically lives under the repo root","On Windows, ensure the repo and .omx are on the same drive root and not accessed via a subst drive letter","If using containers, mount the whole repo rather than just the .omx subdirectory"],"exampleFix":"# before\ncd ~/link-to-repo && run-hook  # sourceCwd is a symlink; realpath escapes\n\n# after\ncd ~/work/myrepo && run-hook  # real path; context resolves inside root","handlingStrategy":"validation","validationCode":"import { realpath, relative } from 'node:fs/promises';\nimport { isAbsolute } from 'node:path';\n\nasync function contextWithinRoot(cwd: string): Promise<boolean> {\n  const root = await realpath(cwd);\n  const ctx = await realpath(join(cwd, '.omx', 'context')).catch(() => null);\n  if (!ctx) return true;\n  const rel = relative(root, ctx);\n  return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);\n}","typeGuard":null,"tryCatchPattern":"try { await ensureAutopilotContextSnapshot(cwd, iso, text); } catch (err) {\n  if ((err as Error).message.includes('escapes repository root')) {\n    const realCwd = await realpath(cwd);\n    return ensureAutopilotContextSnapshot(realCwd, iso, text);\n  }\n  throw err;\n}","preventionTips":["Run tooling from the realpath of the checkout, not a symlinked path","Don't bind-mount or junction .omx to locations outside the repo","On Windows, avoid subst drives for repos; keep repo and state on one drive root"],"tags":["security","path-safety","realpath","mounts"],"backgroundTag":"path-escape-detected","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}