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

[native-assets] cache path escapes configured root

Error message

[native-assets] cache path escapes configured root

What it means

A caller-supplied cache path resolved outside the configured native cache root. The library canonicalizes the cache root (realpath) and requires every managed path to be a descendant; if the path is outside even after resolving symlinks/canonicalization, publication/inspection is refused to prevent writes outside the cache.

Source

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

function lockRecord(token: string, binaryPath: string): string {
  return `${JSON.stringify({
    version: 1,
    token,
    pid: process.pid,
    hostname: hostname(),
    started_at: new Date().toISOString(),
    binary_path: binaryPath,
  })}\n`;
}


function canonicalDescendantPath(path: string, configuredRoot: string, canonicalRoot: string): string {
  const resolvedPath = resolve(path);
  const canonicalRelative = relative(canonicalRoot, resolvedPath);
  if (canonicalRelative && canonicalRelative !== '..' && !canonicalRelative.startsWith(`..${sep}`)) return resolvedPath;
  const rel = relative(resolve(configuredRoot), resolvedPath);
  if (!rel || rel === '..' || rel.startsWith(`..${sep}`)) throw new Error('[native-assets] cache path escapes configured root');
  return join(canonicalRoot, rel);
}

async function canonicalCacheRoot(root: string, create: boolean): Promise<string | undefined> {
  try {
    if (create) await mkdir(root, { recursive: true, mode: 0o700 });
    const entry = await lstat(root);
    if (!entry.isDirectory() && !entry.isSymbolicLink()) throw new Error('unsafe root');
    return await realpath(root); // The configured root itself is intentionally allowed to be a symlink.
  } catch (error) {
    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);

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Pass destinations derived from the cache root the library itself resolves, not absolute hardcoded paths.
  2. Remove symlinks inside the cache directory so realpath stays within the root.
  3. Align the cache-root env override with the paths you pass in.
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative } from 'node:path';
function isInside(root: string, p: string): boolean {
  const rel = relative(resolve(root), resolve(p));
  return rel !== '' && rel !== '..' && !rel.startsWith(`..${require('node:path').sep}`);
}

Try / catch

try { await inspectManagedNativeBinary(dest); } catch (e) { if (/cache path escapes configured root/.test(String(e))) { /* derive dest from the library's cache root */ } throw e; }

Prevention

When it happens

Trigger: inspectManagedNativeBinary or publishManagedNativeBinary with a destination that, once resolved, is not under the canonicalized cache root — e.g. a symlink inside the cache pointing elsewhere, or a custom cache-root env var (NATIVE_CACHE_ROOT-style override) that doesn't contain the destination path.

Common situations: Users overriding the cache root env var but passing hardcoded paths; symlinks inside the cache directory (dotfiles managers like Stow, tmpfs symlinks); CI caches that replace directories with links.

Related errors


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