{"record":{"id":"45caa498a820ac42","repo":"Yeachan-Heo/oh-my-codex","slug":"unsafe-autopilot-context-directory-omx-is-a-symb","errorCode":null,"errorMessage":"Unsafe Autopilot context directory: .omx is a symbolic link","messagePattern":"Unsafe Autopilot context directory: \\.omx is a symbolic link","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/hooks/keyword-detector.ts","lineNumber":365,"sourceCode":"  path: string;\n  kind: AutopilotContextSnapshotKind;\n  original_task_status?: 'activation-prompt' | 'legacy-unverified' | 'unavailable';\n  recovery?: Record<string, unknown>;\n}\n\nconst AUTOPILOT_CONTEXT_RECOVERY_REASON_MESSAGES: Record<AutopilotContextRecoveryReason, string> = {\n  'missing-or-unsafe-legacy-context-snapshot': 'no safe legacy Autopilot context snapshot path was available during continuation.',\n  'missing-autopilot-mode-state': 'active Autopilot skill state existed but no matching Autopilot mode state was available during continuation.',\n  'malformed-autopilot-mode-state': 'active Autopilot mode state could not be parsed during continuation.',\n  'nonpreservable-autopilot-mode-state-missing-current-phase': 'active Autopilot mode state was missing current_phase during continuation.',\n};\n\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,","sourceCodeStart":347,"sourceCodeEnd":383,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/hooks/keyword-detector.ts#L347-L383","documentation":"Thrown by ensureSafeAutopilotContextDir when the .omx directory in the working repo is a symbolic link. Because context snapshots are written inside .omx/context, a symlinked .omx could redirect writes outside the repository, so the hook refuses to proceed.","triggerScenarios":"Running the Autopilot keyword-detector hook in a repo where .omx is a symlink to another directory (e.g. to share state or save disk via ln -s ~/.omx-shared .omx). The check uses lstat, so the link itself is detected even if its target is inside the repo.","commonSituations":"Developers symlinking .omx to a shared/global directory across checkouts; dotfile managers that symlink directories; monorepo setups trying to centralize .omx state; restore from a backup tool that converts directories to links.","solutions":["Remove the symlink and use a real directory: rm .omx && mkdir .omx","If you need shared/centralized state, configure the hook's state directory option if available instead of symlinking","Audit your setup scripts/dotfiles for anything creating the .omx symlink"],"exampleFix":"# before\n.omx -> ~/.shared-omx  (symlink)\n\n# after\nrm .omx\nmkdir .omx","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises';\n\nasync function omxIsSafe(cwd: string): Promise<boolean> {\n  try { return !(await lstat(join(cwd, '.omx'))).isSymbolicLink(); }\n  catch { return true; } // doesn't exist yet; hook will create it\n}","typeGuard":null,"tryCatchPattern":"try { await ensureAutopilotContextSnapshot(cwd, iso, text); } catch (err) {\n  if ((err as Error).message.includes('.omx is a symbolic link')) {\n    await rm(join(cwd, '.omx'), { force: true });\n    await mkdir(join(cwd, '.omx'), { recursive: true });\n    return ensureAutopilotContextSnapshot(cwd, iso, text);\n  }\n  throw err;\n}","preventionTips":["Never symlink .omx to share state across checkouts","Add .omx handling to repo bootstrap scripts to guarantee a real directory","Document that overlay/state sharing must go through supported config, not symlinks"],"tags":["security","symlink","filesystem","path-safety"],"backgroundTag":"symlink-security-check-failed","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}