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
- Find the symlink in the recorded path (`ls -la` on each ancestor) and replace it with a real directory.
- Delete the stale state file and rerun install so paths are re-registered against the current, trusted layout.
- Ensure recorded entry.path values live under an accepted trusted root (correct codexHome); regenerate state if the install location moved.
- 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
- Keep codexHome and its subdirectories as real directories, never symlinks.
- Do not relocate the install directory after recording state; reinstall instead of moving files.
- Audit recorded state paths with `ls -la` for symlinked ancestors before updates.
- Never copy a state file between machines or users; each environment should generate its own.
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
- Invalid ECC repo root: missing package.json at
- Unable to infer ECC repo root from install-state operations
- artifact path escapes output directory
- gate.variant_invalid
- Legacy Codex sync state not found at
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)