affaan-m/ECC · critical · Error
Refusing to ${action} outside the install root: '${targetPat
Error message
Refusing to ${action} outside the install root: '${targetPath}' is not within '${targetRoot}'. What it means
Thrown by assertSafeSkillPath in scripts/lib/install/claude-skill-migration.js — the Claude-skill-specific containment guard used for install, inspect, migrate, and cleanup actions. After computing relativePath = path.relative(targetRoot, targetPath), the guard throws if relativePath is '' (paths identical), starts with '..', or is absolute (which on Windows can indicate a different drive). This refuses writes that would escape the install root.
Source
Thrown at scripts/lib/install/claude-skill-migration.js:56
function comparablePath(filePath) {
const resolvedPath = path.resolve(filePath);
return process.platform === 'win32' ? resolvedPath.toLowerCase() : resolvedPath;
}
function samePath(leftPath, rightPath) {
return comparablePath(leftPath) === comparablePath(rightPath);
}
function assertSafeSkillPath(targetPath, targetRoot, action) {
const resolvedRoot = path.resolve(targetRoot);
const resolvedTarget = path.resolve(targetPath);
const relativePath = path.relative(resolvedRoot, resolvedTarget);
if (
relativePath === ''
|| relativePath.startsWith('..')
|| path.isAbsolute(relativePath)
) {
throw new Error(
`Refusing to ${action} outside the install root: '${targetPath}' is not within '${targetRoot}'.`
);
}
let currentPath = resolvedRoot;
for (const segment of relativePath.split(path.sep)) {
currentPath = path.join(currentPath, segment);
let stats;
try {
stats = fs.lstatSync(currentPath);
} catch (error) {
if (error && error.code === 'ENOENT') {
break;
}
throw error;
}
if (stats.isSymbolicLink()) {
throw new Error(View on GitHub (pinned to 01e15490f0)
Solutions
- Print operation.destinationPath and plan.targetRoot side by side; confirm both are absolute and normalized.
- Ensure destinations are built via path.join(targetRoot, ...) rather than from raw external input.
- On Windows, confirm both paths are on the same drive and use the same casing.
- Discard the suspect install-state and regenerate the plan from the adapter.
Example fix
// before
operations.push({
kind: 'copy-file',
sourcePath,
destinationPath: '/etc/passwd',
});
// after
operations.push({
kind: 'copy-file',
sourcePath,
destinationPath: path.join(targetRoot, 'skills', 'foo', 'SKILL.md'),
}); Defensive patterns
Strategy: validation
Validate before calling
function assertWithin(parent, child) {
const rel = path.relative(path.resolve(parent), path.resolve(child));
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error(`Refusing: ${child} is outside ${parent}`);
}
}
for (const op of plan.operations) assertWithin(plan.targetRoot, op.destinationPath); Try / catch
try {
applyInstallPlan(plan);
} catch (err) {
if (/outside the install root/.test(err.message)) {
console.error('Path escape detected — operation list:', plan.operations);
}
throw err;
} Prevention
- Always derive destination paths via path.join(targetRoot, ...) — never from raw external input.
- On Windows, normalize drive-letter casing before comparing paths.
- Never trust paths read from install-state files; re-derive them from the adapter root.
When it happens
Trigger: A Claude skill operation whose destinationPath resolves above the install root, onto a different drive (Windows), or is identical to the targetRoot itself. Common with a maliciously crafted or corrupted install-state file (see the GHSA note in path-safety.js).
Common situations: A tampered install-state.json that recorded an absolute path outside .claude; Windows drive-letter mismatch; an operation destination built from raw user input without path.join normalization; replaying an old state under a different targetRoot.
Related errors
- Refusing to ${action} through symlinked Claude skill path: '
- Refusing to apply install operation: missing destination pat
- Refusing to install ECC file through symlinked path: '${curr
- Refusing to ${action}: missing destination path.
- Refusing to ${action} '${target}': no trusted install root r
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/d6081183501416b6.
Report an issue: GitHub.