affaan-m/ECC · error

Refusing to through symlinked Claude skill path: ' '.

Error message

Refusing to ${action} through symlinked Claude skill path: '${currentPath}'.

What it means

assertSafeSkillPath walks each path segment from the install root toward the target and refuses to proceed if any intermediate component is a symbolic link (ENOENT segments are tolerated). This prevents skill operations from following symlinks to locations outside the managed root.

Solutions

  1. Replace the symlink with a real directory (copy or move the content) before running the migration.
  2. Temporarily materialize the path: remove the link, copy contents, rerun the operation, then re-create the symlink if needed afterward.
  3. Point the install root at the real (non-symlinked) location where the files physically live.
  4. If only one child is a symlink, unlink and copy that specific skill into place.

Example fix

// before: ~/.claude/skills -> ~/dotfiles/claude-skills (symlink)
// after
rm ~/.claude/skills
cp -r ~/dotfiles/claude-skills ~/.claude/skills
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function hasSymlinkInPath(root, target) {
  let cur = path.resolve(root);
  const dest = path.resolve(target);
  const rel = path.relative(cur, dest);
  for (const seg of rel.split(path.sep)) {
    cur = path.join(cur, seg);
    try { if (fs.lstatSync(cur).isSymbolicLink()) return true; }
    catch (e) { if (e.code === 'ENOENT') break; throw e; }
  }
  return false;
}

Type guard

const isSymlinkFree = (root, target) => !hasSymlinkInPath(root, target);

Try / catch

try {
  await assertSafeClaudeSkillOperation({ action: 'migrate', targetPath, targetRoot });
} catch (error) {
  if (error.message.includes('symlinked Claude skill path')) {
    console.error('Replace the symlink with a real directory, rerun, then re-link if desired');
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: Any skill migration/removal call where the target path — or a parent directory along the way — is a symlink, e.g. skills dir symlinked into a dotfiles repo, or a per-skill symlink pointing elsewhere.

Common situations: Users who symlink ~/.claude/skills into a version-controlled dotfiles directory, or symlink individual skills for sharing; the migration code intentionally refuses to operate through these links.

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


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/eb9afd39b0ff8d7b. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install/claude-skill-migration.js:74

    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(
        `Refusing to ${action} through symlinked Claude skill path: '${currentPath}'.`
      );
    }
  }

  if (pathExists(targetRoot)) {
    assertWithinTrustedRoot(targetPath, targetRoot, action);
  }
}

function describeClaudeSkillOperation(targetRoot, operation) {
  if (!operation || operation.kind !== 'copy-file') {
    return null;
  }

  const sourceRelativePath = normalizeSourceRelativePath(operation.sourceRelativePath);
  if (!sourceRelativePath) {
    return null;

View on GitHub (pinned to 8321021c54)