{"record":{"id":"898aab81bb2ed8f0","repo":"Yeachan-Heo/oh-my-codex","slug":"native-assets-cache-descendant-is-unsafe-curr","errorCode":null,"errorMessage":"[native-assets] cache descendant is unsafe: ${current}","messagePattern":"\\[native-assets\\] cache descendant is unsafe: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/native-assets.ts","lineNumber":393,"sourceCode":"    if (!create && absent(error)) return undefined;\n    throw error;\n  }\n}\n\nasync function validateDescendant(path: string, canonicalRoot: string, createParents: boolean): Promise<void> {\n  const candidate = resolve(path);\n  const rel = relative(canonicalRoot, candidate);\n  if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || resolve(canonicalRoot, rel) !== candidate) {\n    throw new Error('[native-assets] cache path escapes configured root');\n  }\n  const parts = rel.split(sep).filter(Boolean);\n  let current = canonicalRoot;\n  for (const [index, part] of parts.entries()) {\n    current = join(current, part);\n    const isLeaf = index === parts.length - 1;\n    try {\n      const entry = await lstat(current);\n      if (!isLeaf && (entry.isSymbolicLink() || !entry.isDirectory())) throw new Error(`[native-assets] cache descendant is unsafe: ${current}`);\n    } catch (error) {\n      if (!absent(error)) throw error;\n      if (!createParents || isLeaf) continue;\n      await nativeAssetsTestHooks?.beforeCreateParent?.(current);\n      try {\n        await mkdir(current, { mode: 0o700 });\n      } catch (error) {\n        if (errno(error) !== 'EEXIST') throw error;\n      }\n      const created = await lstat(current);\n      if (!created.isDirectory() || created.isSymbolicLink()) throw new Error(`[native-assets] cache descendant is unsafe: ${current}`);\n    }\n  }\n  try {\n    const parent = await realpath(dirname(path));\n    if (parent !== canonicalRoot && !parent.startsWith(`${canonicalRoot}${sep}`)) throw new Error('[native-assets] cache path escapes configured root');\n  } catch (error) {\n    if (!absent(error)) throw error;","sourceCodeStart":375,"sourceCodeEnd":411,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/native-assets.ts#L375-L411","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Delete the offending entry named in the message and let hydration recreate a real directory.","Avoid symlinking inside the native cache; point the whole cache root env var at the desired location instead.","Clear the cache (rm -rf of the configured root) as a blunt fix."],"exampleFix":"# before: ~/.cache/omx-native/bin -> /shared/omx-bin\nrm ~/.cache/omx-native/bin\n# after: real directory recreated by hydration; move sharing to the root via env override","handlingStrategy":"validation","validationCode":"import { lstatSync } from 'node:fs';\nimport { join } from 'node:path';\nfunction cacheSegmentsAreDirs(root: string, rel: string): boolean {\n  let cur = root;\n  for (const part of rel.split(/[\\\\/]/).filter(Boolean).slice(0, -1)) {\n    cur = join(cur, part);\n    try { const st = lstatSync(cur); if (!st.isDirectory() || st.isSymbolicLink()) return false; } catch { /* absent ok */ }\n  }\n  return true;\n}","typeGuard":null,"tryCatchPattern":"try { await hydrateNativeBinary(); } catch (e) { if (/cache descendant is unsafe/.test(String(e))) { /* remove the printed symlink/file, retry */ } throw e; }","preventionTips":["Never symlink inside the native cache","Relocate the entire cache via its env override instead","Clear the cache when in doubt — it rehydrates"],"tags":["native-assets","cache","symlink","filesystem-security"],"backgroundTag":"symlink-in-path","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}