affaan-m/ECC · error · Error

Refusing to ${action}: destination parent is not a trusted d

Error message

Refusing to ${action}: destination parent is not a trusted directory.

What it means

Thrown by ensureContainedParentDir after mkdir'ing each path segment: the freshly created (or pre-existing) parent is re-stat'd with lstat and rejected if it is not a directory OR is a symbolic link. This is a TOCTOU / symlink-hardening guard so an attacker cannot pre-place a symlink between mkdir and the subsequent write inside the trusted root. The ${action} token reflects the caller's verb (install, repair, uninstall).

Source

Thrown at scripts/lib/install-lifecycle.js:297

    ? relativeParent.split(path.sep).filter(Boolean)
    : [];
  let currentPath = canonicalRoot;

  for (const segment of pathSegments) {
    const validatedParent = assertWithinTrustedRoot(currentPath, canonicalRoot, action);
    const nextPath = path.join(validatedParent, segment);
    try {
      fs.mkdirSync(nextPath);
    } catch (error) {
      if (!error || error.code !== 'EEXIST') {
        throw error;
      }
    }

    const validatedNext = assertWithinTrustedRoot(nextPath, canonicalRoot, action);
    const nextStat = fs.lstatSync(validatedNext);
    if (!nextStat.isDirectory() || nextStat.isSymbolicLink()) {
      throw new Error(`Refusing to ${action}: destination parent is not a trusted directory.`);
    }
    currentPath = validatedNext;
  }

  return getManagedDestination(managedPath, canonicalRoot, action).managedPath;
}

function prepareContainedWriteDestination(destinationPath, trustedRoot, action) {
  return ensureContainedParentDir(destinationPath, trustedRoot, action);
}

function getContainedExistingPath(
  destinationPath,
  trustedRoot,
  action,
  { allowFinalSymlink = false } = {}
) {
  const initialDestination = getManagedDestination(

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Inspect each segment of the destination's parent chain with `ls -la` and replace any symlink with a real directory: `rm <symlink> && mkdir -p <dir>`.
  2. Re-run the install/repair/uninstall action once the chain contains only real directories.
  3. If the symlink is intentional, point the installer at a target root whose parent chain is all real directories (override HOME or the target root).
  4. Check for a concurrent process (cloud-sync, backup) racing the installer and pause it during the operation.

Example fix

// before: ~/.claude/skills -> /mnt/shared/skills (symlink)
// after:
rm ~/.claude/skills
mkdir -p ~/.claude/skills
./install.sh --target claude
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const path = require('path');

function verifyRealDirChain(targetPath, trustedRoot) {
  const resolved = path.resolve(trustedRoot);
  let cur = path.resolve(targetPath);
  while (cur.startsWith(resolved) && cur !== resolved) {
    const st = fs.lstatSync(cur);
    if (!st.isDirectory() || st.isSymbolicLink()) {
      throw new Error(`Unsafe segment (not a real dir or is symlink): ${cur}`);
    }
    cur = path.dirname(cur);
  }
}
// call before invoking install/repair/uninstall
verifyRealDirChain('/home/user/.claude/skills', '/home/user/.claude');

Type guard

function isRealDirectory(stat) {
  return stat.isDirectory() && !stat.isSymbolicLink();
}

Try / catch

try {
  await runInstall(target);
} catch (err) {
  if (/Refusing to .*: destination parent is not a trusted directory/.test(err.message)) {
    // walk the chain, replace symlinks with real dirs, then retry once
  } else throw err;
}

Prevention

When it happens

Trigger: Calling any contained-write path (writeContainedFile, copyContainedFile, prepareContainedWriteDestination) where an intermediate directory segment is a symlink, a regular file, a device, or got swapped to a symlink between mkdirSync and lstatSync. Also triggers if the destination's parent chain leaves the trusted root because a segment was replaced.

Common situations: A user pre-created ~/.claude/skills as a symlink to a shared drive; running ./install.sh --target claude then fails because the lifecycle refuses to descend through it. Another case: a sync tool (Dropbox, iCloud) replaced an intermediate dir with a symlink. Mis-configured HOME pointing at a path where a parent is a symlink that should be a real directory.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/66d56448fc768481. Report an issue: GitHub.