affaan-m/ECC · error · Error

Refusing to reuse unsafe legacy Codex ownership path: ${file

Error message

Refusing to reuse unsafe legacy Codex ownership path: ${filePath}

What it means

During reinstall, assertInstalledStateUnmodified() replays every recorded path. For each entry it resolves a trusted root (codexHome, recorded trustedRoots, or the installedHooksPath) and refuses the entry when the file no longer sits under any trusted root, or when a directory between the root and the file is a symlink (hasUnsafeManagedAncestor walks each segment). This is a deliberate guard: recorded ownership must never be reused through a path that escaped the managed tree.

Source

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

function snapshotLegacyPath(filePath) {
  const snapshot = readRegularFileNoFollow(filePath);
  const previousType = snapshot ? 'file' : 'missing';
  return {
    path: filePath,
    installedSha256: null,
    previousType,
    previousContentBase64: snapshot ? snapshot.content.toString('base64') : null,
    previousMode: snapshot ? snapshot.mode : null,
  };
}

function assertInstalledStateUnmodified(state) {
  for (const entry of state.paths) {
    const filePath = path.resolve(entry.path);
    const trustedRoot = getTrustedRoot(state, filePath);
    if (!trustedRoot || hasUnsafeManagedAncestor(filePath, trustedRoot)) {
      throw new Error(`Refusing to reuse unsafe legacy Codex ownership path: ${filePath}`);
    }
    const snapshot = readRegularFileNoFollow(filePath);
    if (!entry.installedSha256) {
      if (snapshot) throw new Error(`Refusing to replace modified legacy Codex artifact: ${filePath}`);
      continue;
    }
    const digest = snapshot
      ? crypto.createHash('sha256').update(snapshot.content).digest('hex')
      : null;
    if (digest !== entry.installedSha256) {
      throw new Error(`Refusing to replace modified legacy Codex artifact: ${filePath}`);
    }
  }
}

function beginLegacySyncState(options) {
  const codexHome = path.resolve(options.codexHome);
  const statePath = getStatePath(codexHome);

View on GitHub (pinned to 06c5e118c4)

Solutions

  1. Open the state file and list each entry.path; identify which no longer live under the current codexHome / trustedRoots / hooks path
  2. Restore the original layout (same absolute codexHome, no symlinked ancestors) and re-run
  3. If the entries are stale, back up and delete the state file so the next sync rebuilds ownership from scratch
  4. Replace any symlinked ancestor directory with a real directory before re-running

Example fix

// before: ~/.codex/hooks is a symlink -> Refusing to reuse unsafe legacy Codex ownership path
fs.rmSync(path.join(codexHome, 'hooks')); // removes the symlink only
fs.mkdirSync(path.join(codexHome, 'hooks'), { recursive: true });
// after: re-run the legacy Codex sync
Defensive patterns

Strategy: validation

Validate before calling

const path = require('path'), fs = require('fs');
function entriesAreWithinRoots(entries, roots) {
  const resolved = roots.map(r => path.resolve(r));
  return entries.every(e => {
    const p = path.resolve(e.path);
    return resolved.some(r => p === r || p.startsWith(r + path.sep));
  });
}
function hasSymlinkedAncestor(filePath, root) {
  const segs = path.relative(root, filePath).split(path.sep).slice(0, -1);
  let cur = root;
  for (const seg of segs) {
    cur = path.join(cur, seg);
    if (fs.lstatSync(cur).isSymbolicLink()) return true;
  }
  return false;
}

Try / catch

try { beginLegacySyncState({ codexHome }); }
catch (err) {
  if (/unsafe legacy Codex ownership path/.test(err.message)) {
    // print the offending entry.path and the trusted roots so the operator fixes the layout
  } else throw err;
}

Prevention

When it happens

Trigger: Re-running the legacy Codex sync when a state entry references an absolute path outside the current trusted roots (CODEX_HOME moved/renamed after install, state file copied from another machine), or when an ancestor directory inside the trusted root was replaced by a symlink.

Common situations: Renaming or moving ~/.codex after the first install; dotfile managers replacing config subdirectories with symlinks; state created under a different user or container path.

Related errors


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