affaan-m/ECC · error · Error

Refusing to manage legacy sync path through symlinked ancest

Error message

Refusing to manage legacy sync path through symlinked ancestor: ${filePath}

What it means

recordLegacySyncPath() also runs hasUnsafeManagedAncestor(): it walks every directory segment between the trusted root and the target file and refuses when any segment is a symlink. Managing a path through a symlinked directory could redirect writes outside the tree the operator granted, so the recording is rejected.

Source

Thrown at scripts/lib/codex-legacy-sync.js:282

  for (const [key, filePath] of [['config', configPath], ['agents', agentsPath]]) {
    if (priorState) break;
    const snapshot = readRegularFileNoFollow(filePath, 'utf8');
    state.before[key] = snapshot ? snapshot.content : null;
  }
  atomicWriteJson(statePath, state);
  return statePath;
}

function recordLegacySyncPath(options) {
  const state = readState(options.statePath);
  const filePath = path.resolve(options.filePath);
  const trustedRoot = getTrustedRoot(state, filePath);
  if (!trustedRoot) {
    throw new Error(`Refusing to record a legacy sync path outside trusted roots: ${filePath}`);
  }
  if (hasUnsafeManagedAncestor(filePath, trustedRoot)) {
    throw new Error(`Refusing to manage legacy sync path through symlinked ancestor: ${filePath}`);
  }
  if (!state.paths.some(entry => entry.path === filePath)) {
    const snapshot = snapshotLegacyPath(filePath);
    state.paths.push(snapshot);
    state.rollbackPaths = [...(state.rollbackPaths || []), { ...snapshot }];
    atomicWriteJson(options.statePath, state);
  }
}

function rollbackLegacyCodexSync(options) {
  const state = readState(options.statePath);
  const restoredPaths = [];
  const retainedPaths = [];

  const rollbackPaths = Array.isArray(state.rollbackPaths) ? state.rollbackPaths : state.paths;
  for (const entry of [...rollbackPaths].reverse()) {
    const filePath = path.resolve(entry.path);
    const trustedRoot = getTrustedRoot(state, filePath);

View on GitHub (pinned to 06c5e118c4)

Solutions

  1. Replace the symlinked directory with a real directory (move the target's contents in) and re-run
  2. Point codexHome directly at the symlink's real target so no symlink sits inside the managed tree
  3. Record a path that does not traverse the symlinked ancestor

Example fix

# before: ~/.codex/hooks is a symlink -> Refusing to manage legacy sync path through symlinked ancestor
rm ~/.codex/hooks && mkdir ~/.codex/hooks && cp ~/dotfiles/hooks/* ~/.codex/hooks/
# after: re-run the sync
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs'), path = require('path');
function hasSymlinkedAncestor(filePath, root) {
  const segs = path.relative(root, path.resolve(filePath)).split(path.sep).slice(0, -1);
  let cur = path.resolve(root);
  for (const seg of segs) {
    cur = path.join(cur, seg);
    if (fs.lstatSync(cur).isSymbolicLink()) return true;
  }
  return false;
}
if (hasSymlinkedAncestor(filePath, codexHome)) fixLayoutBeforeRecording(filePath);

Prevention

When it happens

Trigger: Recording a file whose parent chain inside codexHome contains a symlink — e.g. ~/.codex is real but ~/.codex/hooks -> ~/dotfiles/hooks, and the caller records ~/.codex/hooks/hook.json.

Common situations: Dotfiles managers symlinking config directories; container images linking CODEX_HOME subdirectories to volumes; stow-managed home directories.

Related errors


AI-assisted analysis of affaan-m/ECC@06c5e118c4 (2026-08-18). Data as JSON: /api/errors/79e23e4fb073ff04. Report an issue: GitHub.