affaan-m/ECC · error · Error
Refusing to manage legacy sync path through symlinked…
Error message
Refusing to manage legacy sync path through symlinked ancestor: ${filePath} What it means
recordLegacySyncPath rejects file paths that have a symlinked ancestor between the file and its trusted root (hasUnsafeManagedAncestor). Managing files through symlinked directories could cause the tool to write to an unexpected location, so it fails closed.
Solutions
- Remove the symlink in the ancestor chain and use a real directory (or symlink the file itself, not an ancestor)
- Pass the fully resolved real path whose ancestors are actual directories
- Use a trusted root that is itself the real (non-symlinked) location of the managed files
- Re-run after fixing the directory layout
Example fix
// before
recordLegacySyncPath({ statePath, filePath: '/home/me/.codex/config.toml' }) // ~/.codex -> /dotfiles/codex
// after
// manage the real location, whose ancestors are not symlinks:
recordLegacySyncPath({ statePath, filePath: '/dotfiles/codex/config.toml' }); Defensive patterns
Strategy: validation
Validate before calling
function hasSymlinkAncestor(p, stop) {
let cur = path.dirname(path.resolve(p));
const end = path.resolve(stop);
while (cur !== end && cur !== path.parse(cur).root) {
if (fs.lstatSync(cur).isSymbolicLink()) return true;
cur = path.dirname(cur);
}
return false;
} Type guard
const safeToManage = (p, root) => !hasSymlinkAncestor(p, root);
Try / catch
try { recordLegacySyncPath(opts); } catch (e) {
if (String(e.message).includes('symlinked ancestor')) console.error('Resolve symlinked directories before syncing', opts.filePath);
else throw e;
} Prevention
- Avoid symlink-farm dotfile managers for managed Codex dirs, or sync the real directory
- Run fs.realpathSync on candidate paths before passing them
- Audit ~/.codex and ancestors with `ls -la` for symlinks
- Prefer hard copies over directory symlinks for tool-managed config
When it happens
Trigger: Calling recordLegacySyncPath with a filePath where some ancestor directory (between filePath and the trusted root) is a symlink — detected by hasUnsafeManagedAncestor during path recording.
Common situations: Codex home or a managed subdirectory is a symlink (e.g. dotfiles managed via symlink farms like GNU Stow); passing /tmp-based symlinked paths; a macOS environment where ~/Library or similar is symlinked.
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
- Unsafe state-store path
- artifact path must be canonical and absolute
- gate.variant_invalid
- Invalid ECC repo root: missing package.json at
- Nasiko executable must be a regular file, not a symlink.
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/79e23e4fb073ff04.
Report an issue: GitHub.
Appendix: 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 8321021c54)