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
- 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>`.
- Re-run the install/repair/uninstall action once the chain contains only real directories.
- 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).
- 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
- Never pre-create install destinations as symlinks; use real directories.
- Pause cloud-sync clients (Dropbox/iCloud) before running the installer.
- Run the installer as the same user that owns the target root so segment ownership is stable.
- If sharing content across machines, sync the source repo, not the installed target.
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
- Refusing to read non-file path: ${filePath}
- Refusing to access memory through symlink root: ${root}
- Refusing to access memory through symlink directory: ${direc
- Refusing to install ECC file through symlinked path: '${curr
- ${label} must remain a regular, non-symlink file while it is
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/66d56448fc768481.
Report an issue: GitHub.