Yeachan-Heo/oh-my-codex · error · Error

[native-assets] cache descendant is unsafe: ${current}

Error message

[native-assets] cache descendant is unsafe: ${current}

What it means

While walking each path segment under the cache root, an intermediate (non-leaf) entry exists but is a symlink or not a directory. The hydration pipeline refuses to traverse through symlinks or files to reach the destination, as a hardening measure against symlink-swap attacks on multi-user caches.

Source

Thrown at src/cli/native-assets.ts:393

    if (!create && absent(error)) return undefined;
    throw error;
  }
}

async function validateDescendant(path: string, canonicalRoot: string, createParents: boolean): Promise<void> {
  const candidate = resolve(path);
  const rel = relative(canonicalRoot, candidate);
  if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || resolve(canonicalRoot, rel) !== candidate) {
    throw new Error('[native-assets] cache path escapes configured root');
  }
  const parts = rel.split(sep).filter(Boolean);
  let current = canonicalRoot;
  for (const [index, part] of parts.entries()) {
    current = join(current, part);
    const isLeaf = index === parts.length - 1;
    try {
      const entry = await lstat(current);
      if (!isLeaf && (entry.isSymbolicLink() || !entry.isDirectory())) throw new Error(`[native-assets] cache descendant is unsafe: ${current}`);
    } catch (error) {
      if (!absent(error)) throw error;
      if (!createParents || isLeaf) continue;
      await nativeAssetsTestHooks?.beforeCreateParent?.(current);
      try {
        await mkdir(current, { mode: 0o700 });
      } catch (error) {
        if (errno(error) !== 'EEXIST') throw error;
      }
      const created = await lstat(current);
      if (!created.isDirectory() || created.isSymbolicLink()) throw new Error(`[native-assets] cache descendant is unsafe: ${current}`);
    }
  }
  try {
    const parent = await realpath(dirname(path));
    if (parent !== canonicalRoot && !parent.startsWith(`${canonicalRoot}${sep}`)) throw new Error('[native-assets] cache path escapes configured root');
  } catch (error) {
    if (!absent(error)) throw error;

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Delete the offending entry named in the message and let hydration recreate a real directory.
  2. Avoid symlinking inside the native cache; point the whole cache root env var at the desired location instead.
  3. Clear the cache (rm -rf of the configured root) as a blunt fix.

Example fix

# before: ~/.cache/omx-native/bin -> /shared/omx-bin
rm ~/.cache/omx-native/bin
# after: real directory recreated by hydration; move sharing to the root via env override
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync } from 'node:fs';
import { join } from 'node:path';
function cacheSegmentsAreDirs(root: string, rel: string): boolean {
  let cur = root;
  for (const part of rel.split(/[\\/]/).filter(Boolean).slice(0, -1)) {
    cur = join(cur, part);
    try { const st = lstatSync(cur); if (!st.isDirectory() || st.isSymbolicLink()) return false; } catch { /* absent ok */ }
  }
  return true;
}

Try / catch

try { await hydrateNativeBinary(); } catch (e) { if (/cache descendant is unsafe/.test(String(e))) { /* remove the printed symlink/file, retry */ } throw e; }

Prevention

When it happens

Trigger: validateDescendant with createParents where some intermediate component of the destination path is a symbolic link or a regular file, e.g. cacheDir/bin being a symlink to another location.

Common situations: Users symlinking cache subdirectories to shared storage; partial/corrupt cache left by a killed process where a directory became a file; dotfile managers placing links inside the cache.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/898aab81bb2ed8f0. Report an issue: GitHub.