affaan-m/ECC · error · Error

Refusing to reuse unsafe legacy Codex ownership path

Error message

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

What it means

assertInstalledStateUnmodified re-verifies every path recorded in the sync state before the installer reuses them. For each entry it resolves a trusted root and rejects the whole operation if the path has no trusted root or if any managed ancestor directory is unsafe (hasUnsafeManagedAncestor, e.g. a symlinked ancestor). This prevents reusing ownership of paths that could be redirected through symlinks to write outside the trusted root — a path-traversal/symlink-swap defense.

Solutions

  1. Find the symlink in the recorded path (`ls -la` on each ancestor) and replace it with a real directory.
  2. Delete the stale state file and rerun install so paths are re-registered against the current, trusted layout.
  3. Ensure recorded entry.path values live under an accepted trusted root (correct codexHome); regenerate state if the install location moved.
  4. If you believe the path is genuinely safe, verify each ancestor with a symlink audit and only then recreate the state — do not bypass the check.

Example fix

// before: ~/.codex/prompts is a symlink to /tmp/prompts
mv /tmp/prompts ~/.codex/prompts.real
rm ~/.codex/prompts && mv ~/.codex/prompts.real ~/.codex/prompts
// after: real directory, no symlinked ancestors; rerun install
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
import path from 'node:path';
function hasSymlinkAncestor(filePath) {
  let dir = path.dirname(path.resolve(filePath));
  while (true) {
    if (fs.lstatSync(dir).isSymbolicLink()) return true;
    const parent = path.dirname(dir);
    if (parent === dir) return false;
    dir = parent;
  }
}

Try / catch

try {
  beginLegacySyncState(options);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Refusing to reuse unsafe legacy Codex ownership path')) {
    const p = err.message.split('path: ')[1];
    console.error(`Fix symlinked/tampered ancestors of ${p}, then rerun install`);
    process.exitCode = 1;
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: beginLegacySyncState -> assertInstalledStateUnmodified over a state whose entry.path resolves outside any trusted root (getTrustedRoot returns null), or whose resolved path has an unsafe (e.g. symlinked) managed ancestor directory relative to that root.

Common situations: A directory in the path (e.g. ~/.codex or a subdirectory) was replaced with a symlink; the state file was moved/copied between machines so recorded absolute paths no longer sit under a trusted root; project relocated and paths recorded with a different prefix; attacker tampering with managed directories.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/ab549c444da0718c. Report an issue: GitHub.

Appendix: 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 8321021c54)