PrefectHQ/fastmcp · error · ValueError

Skill name {skill_name!r} would escape the target directory

Error message

Skill name {skill_name!r} would escape the target directory

What it means

`download_skill` resolves the skill directory as `target_dir / skill_name` and performs a path-traversal security check: if the resolved skill directory is not inside the resolved target directory, this ValueError is raised. It prevents skill names like `../..` or absolute paths from writing files outside the intended destination.

Source

Thrown at fastmcp_slim/fastmcp/utilities/skills.py:171

        ```python
        from fastmcp import Client
        from fastmcp.utilities.skills import download_skill

        async with Client("http://skills-server/mcp") as client:
            skill_path = await download_skill(
                client,
                "pdf-processing",
                "~/.claude/skills"
            )
            print(f"Downloaded to: {skill_path}")
        ```
    """
    target_dir = Path(target_dir).expanduser().resolve()
    skill_dir = (target_dir / skill_name).resolve()

    # Security: ensure skill_dir stays within target_dir
    if not skill_dir.is_relative_to(target_dir):
        raise ValueError(f"Skill name {skill_name!r} would escape the target directory")

    # Check if directory exists
    if skill_dir.exists() and not overwrite:
        raise FileExistsError(
            f"Skill directory already exists: {skill_dir}. "
            "Use overwrite=True to replace."
        )

    # Get manifest to know what files to download
    manifest = await get_skill_manifest(client, skill_name)

    # Create skill directory
    skill_dir.mkdir(parents=True, exist_ok=True)

    # Download each file
    for file_info in manifest.files:
        # Security: reject absolute paths and paths that escape skill_dir
        if Path(file_info.path).is_absolute():

View on GitHub (pinned to 1f02114297)

Solutions

  1. Sanitize the skill name to a bare directory name: strip `/`, `\\`, and `..` segments before calling `download_skill`.
  2. If nested layout is desired, create subdirectories under `target_dir` yourself and pass a name that stays within it (note: the guard still requires the resolved dir to remain inside target).
  3. Treat this as a signal of malicious or malformed input — log and reject rather than trying to bypass the check.

Example fix

// before
await download_skill(client, "../../etc/pwned", "/tmp/skills")
// after
safe_name = skill_name.replace("/", "_")
await download_skill(client, safe_name, "/tmp/skills")
Defensive patterns

Strategy: validation

Validate before calling

import re
_SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")

def skill_name_is_safe(name: str) -> bool:
    return bool(_SAFE_NAME.match(name)) and ".." not in name

Type guard

import re

def is_plain_dirname(v: object) -> bool:
    return isinstance(v, str) and re.fullmatch(r"[^/\\]+", v) is not None and v not in (".", "..")

Try / catch

try:
    await download_skill(client, skill_name, target_dir)
except ValueError as e:
    if "would escape the target directory" in str(e):
        raise UnsafeSkillNameError(skill_name) from e
    raise

Prevention

When it happens

Trigger: Calling `download_skill(client, skill_name, target_dir)` where `skill_name` contains path separators or `..` segments (e.g. `"../evil"`, `"a/b"` escaping the target, an absolute path), so the resolved skill dir falls outside `target_dir`.

Common situations: Skill names sourced from untrusted input (user-supplied lists, remote manifests, DB rows) containing slashes or traversal segments, or callers programmatically building names with subdirectory components expecting nesting that the security check forbids.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/59d78157b9eb7ed3. Report an issue: GitHub.