affaan-m/ECC · error
Refusing to outside the install root: ' ' is not within ' '.
Error message
Refusing to ${action} outside the install root: '${targetPath}' is not within '${targetRoot}'. What it means
assertSafeSkillPath resolves the target path and verifies it lies strictly inside the install root (not the root itself, not escaping via .., not absolute after normalization) before any skill file operation. This is a path-traversal guard: migration/removal code must never touch files outside the Claude skills root.
Solutions
- Check the targetPath value in the error and correct it so it resolves inside the install root.
- Verify the install root configuration points at the actual ~/.claude (or equivalent) skills directory.
- If the path comes from stored state (previous install records), delete or regenerate that stale state.
- Never bypass by passing raw user input; normalize/resolve relative paths against the correct root first.
Example fix
// before await removeLegacyClaudeSkillFiles(root, '/etc/hosts'); // after await removeLegacyClaudeSkillFiles(root, path.join(root, 'skills', 'old-skill', 'SKILL.md'));
Defensive patterns
Strategy: validation
Validate before calling
const path = require('path');
function isInsideRoot(root, target) {
const rel = path.relative(path.resolve(root), path.resolve(target));
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
} Type guard
const isSafeSkillPath = (root, target) => isInsideRoot(root, target);
Try / catch
try {
await assertSafeClaudeSkillOperation({ action: 'remove', targetPath, targetRoot });
} catch (error) {
if (error.message.includes('outside the install root')) {
console.error(`Resolve ${targetPath} under ${targetRoot} before retrying`);
return;
}
throw error;
} Prevention
- Never pass raw user/CLI-supplied paths into skill operations
- Resolve relative paths against the install root before calling
- Verify install-root configuration points at the real skills directory
- Regenerate stale recorded paths from previous installs rather than reusing them
When it happens
Trigger: Calling assertSafeClaudeSkillOperation (or the skill migration/cleanup helpers) with a targetPath that resolves outside targetRoot — e.g. containing ../ segments, being a symlink target outside the root, or passing an absolute path in another directory.
Common situations: Misconfigured install root (CLAUDE config pointing elsewhere), corrupted stored skill paths containing ../, or passing a user-supplied path from a CLI argument straight into the migration API.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Refusing to outside the install root: ' ' is not within ' '.
- Refusing to through symlinked Claude skill path: ' '.
- refusing to remove : escapes
- artifact must be a resident regular file
- artifact path escapes output directory
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/d6081183501416b6.
Report an issue: GitHub.
Appendix: 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 8321021c54)