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

Unsafe Autopilot context directory: .omx is a symbolic link

Error message

Unsafe Autopilot context directory: .omx is a symbolic link

What it means

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.

Source

Thrown at src/hooks/keyword-detector.ts:365

  path: string;
  kind: AutopilotContextSnapshotKind;
  original_task_status?: 'activation-prompt' | 'legacy-unverified' | 'unavailable';
  recovery?: Record<string, unknown>;
}

const AUTOPILOT_CONTEXT_RECOVERY_REASON_MESSAGES: Record<AutopilotContextRecoveryReason, string> = {
  'missing-or-unsafe-legacy-context-snapshot': 'no safe legacy Autopilot context snapshot path was available during continuation.',
  'missing-autopilot-mode-state': 'active Autopilot skill state existed but no matching Autopilot mode state was available during continuation.',
  'malformed-autopilot-mode-state': 'active Autopilot mode state could not be parsed during continuation.',
  'nonpreservable-autopilot-mode-state-missing-current-phase': 'active Autopilot mode state was missing current_phase during continuation.',
};

async function ensureSafeAutopilotContextDir(sourceCwd: string): Promise<string> {
  const rootRealPath = await realpath(sourceCwd);
  const omxDir = join(sourceCwd, '.omx');
  await mkdir(omxDir, { recursive: true });
  if ((await lstat(omxDir)).isSymbolicLink()) {
    throw new Error('Unsafe Autopilot context directory: .omx is a symbolic link');
  }

  const contextDir = join(omxDir, 'context');
  await mkdir(contextDir, { recursive: true });
  if ((await lstat(contextDir)).isSymbolicLink()) {
    throw new Error('Unsafe Autopilot context directory: .omx/context is a symbolic link');
  }

  const contextRealPath = await realpath(contextDir);
  const relativeToRoot = relative(rootRealPath, contextRealPath);
  if (relativeToRoot === '' || relativeToRoot.startsWith('..') || isAbsolute(relativeToRoot)) {
    throw new Error('Unsafe Autopilot context directory: resolved path escapes repository root');
  }
  return contextDir;
}

async function writeUniqueAutopilotContextSnapshot(
  sourceCwd: string,

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Remove the symlink and use a real directory: rm .omx && mkdir .omx
  2. If you need shared/centralized state, configure the hook's state directory option if available instead of symlinking
  3. Audit your setup scripts/dotfiles for anything creating the .omx symlink

Example fix

# before
.omx -> ~/.shared-omx  (symlink)

# after
rm .omx
mkdir .omx
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises';

async function omxIsSafe(cwd: string): Promise<boolean> {
  try { return !(await lstat(join(cwd, '.omx'))).isSymbolicLink(); }
  catch { return true; } // doesn't exist yet; hook will create it
}

Try / catch

try { await ensureAutopilotContextSnapshot(cwd, iso, text); } catch (err) {
  if ((err as Error).message.includes('.omx is a symbolic link')) {
    await rm(join(cwd, '.omx'), { force: true });
    await mkdir(join(cwd, '.omx'), { recursive: true });
    return ensureAutopilotContextSnapshot(cwd, iso, text);
  }
  throw err;
}

Prevention

When it happens

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

Common situations: 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.

Related errors


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