langchain-ai/deepagents · error · SystemExit

Error: Skill directory is a symlink. Refusing to delete for

Error message

Error: Skill directory is a symlink. Refusing to delete for safety.

What it means

Just before performing the removal (after confirmation, allowing for the pause at the prompt), `_delete` re-checks `skill_dir.is_symlink()`. A symlinked skill directory could delete the target directory's contents elsewhere on disk, so the command hard-refuses as a safety measure.

Source

Thrown at libs/code/deepagents_code/skills/commands.py:835

        )
        try:
            response = input().strip().lower()
        except (EOFError, KeyboardInterrupt):
            console.print("\n[dim]Cancelled.[/dim]")
            return

        if response not in {"y", "yes"}:
            console.print("[dim]Cancelled.[/dim]")
            return

    # Re-validate immediately before deletion to narrow the TOCTOU window
    # (the user may have paused at the confirmation prompt).
    if skill_dir.is_symlink():
        console.print(
            "[bold red]Error:[/bold red] Skill directory is a symlink. "
            "Refusing to delete for safety."
        )
        raise SystemExit(1)

    is_valid_path, path_error = _validate_skill_path(skill_dir, base_dir)
    if not is_valid_path:
        console.print(f"[bold red]Error:[/bold red] {path_error}")
        raise SystemExit(1)

    # Delete the skill directory
    try:
        shutil.rmtree(skill_dir)
    except OSError as e:
        console.print(
            f"[bold red]Error:[/bold red] Failed to fully delete skill: {e}\n"
            f"[yellow]Warning:[/yellow] Some files may have been partially removed.\n"
            f"Please inspect: {skill_dir}/"
        )
        raise SystemExit(1) from e

    if output_format == "json":

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Remove the symlink manually (`rm` the link, not `rm -r`) and manage the real directory in its source location
  2. Copy the real files into the skills directory (`cp -rL`) so it is no longer a symlink, then delete via the CLI
  3. Check `ls -la` / `readlink` to confirm which path is the link before removing

Example fix

// before
dcode skills delete my-skill   # ~/.claude/skills/my-skill is a stow symlink
// after
unlink ~/.claude/skills/my-skill   # remove link; edit real files in ~/dotfiles
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def is_symlinked_skill(skills_root: Path, name: str) -> bool:
    return (skills_root / name).is_symlink()

if is_symlinked_skill(Path(".claude/skills"), "my-skill"):
    raise SystemExit("remove the symlink manually; CLI refuses to delete links")

Type guard

def deletable_skill_dir(root: Path, name: str) -> bool:
    p = root / name
    return p.is_dir() and not p.is_symlink()

Try / catch

proc = subprocess.run(["dcode", "skills", "delete", name, "--force"], capture_output=True, text=True)
if proc.returncode != 0 and "symlink" in proc.stderr:
    os.unlink(skill_link)  # remove the link itself, manage real files elsewhere

Prevention

When it happens

Trigger: `_delete` where `skill_dir.is_symlink()` is True at deletion time — typically dotfile-manager-managed skills (GNU stow, chezmoi), or manually symlinked folders into a shared repo.

Common situations: Users who manage skills via symlinked dotfiles try to delete through the CLI; the fix is to remove the symlink itself or manage the real files in their source location.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/428d2e67efaf8cf9. Report an issue: GitHub.