{"record":{"id":"59d78157b9eb7ed3","repo":"PrefectHQ/fastmcp","slug":"skill-name-skill-name-r-would-escape-the-target","errorCode":null,"errorMessage":"Skill name {skill_name!r} would escape the target directory","messagePattern":"Skill name (.+?) would escape the target directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/utilities/skills.py","lineNumber":171,"sourceCode":"        ```python\n        from fastmcp import Client\n        from fastmcp.utilities.skills import download_skill\n\n        async with Client(\"http://skills-server/mcp\") as client:\n            skill_path = await download_skill(\n                client,\n                \"pdf-processing\",\n                \"~/.claude/skills\"\n            )\n            print(f\"Downloaded to: {skill_path}\")\n        ```\n    \"\"\"\n    target_dir = Path(target_dir).expanduser().resolve()\n    skill_dir = (target_dir / skill_name).resolve()\n\n    # Security: ensure skill_dir stays within target_dir\n    if not skill_dir.is_relative_to(target_dir):\n        raise ValueError(f\"Skill name {skill_name!r} would escape the target directory\")\n\n    # Check if directory exists\n    if skill_dir.exists() and not overwrite:\n        raise FileExistsError(\n            f\"Skill directory already exists: {skill_dir}. \"\n            \"Use overwrite=True to replace.\"\n        )\n\n    # Get manifest to know what files to download\n    manifest = await get_skill_manifest(client, skill_name)\n\n    # Create skill directory\n    skill_dir.mkdir(parents=True, exist_ok=True)\n\n    # Download each file\n    for file_info in manifest.files:\n        # Security: reject absolute paths and paths that escape skill_dir\n        if Path(file_info.path).is_absolute():","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/utilities/skills.py#L153-L189","documentation":"`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.","triggerScenarios":"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`.","commonSituations":"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.","solutions":["Sanitize the skill name to a bare directory name: strip `/`, `\\\\`, and `..` segments before calling `download_skill`.","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).","Treat this as a signal of malicious or malformed input — log and reject rather than trying to bypass the check."],"exampleFix":"// before\nawait download_skill(client, \"../../etc/pwned\", \"/tmp/skills\")\n// after\nsafe_name = skill_name.replace(\"/\", \"_\")\nawait download_skill(client, safe_name, \"/tmp/skills\")","handlingStrategy":"validation","validationCode":"import re\n_SAFE_NAME = re.compile(r\"^[A-Za-z0-9._-]+$\")\n\ndef skill_name_is_safe(name: str) -> bool:\n    return bool(_SAFE_NAME.match(name)) and \"..\" not in name","typeGuard":"import re\n\ndef is_plain_dirname(v: object) -> bool:\n    return isinstance(v, str) and re.fullmatch(r\"[^/\\\\]+\", v) is not None and v not in (\".\", \"..\")","tryCatchPattern":"try:\n    await download_skill(client, skill_name, target_dir)\nexcept ValueError as e:\n    if \"would escape the target directory\" in str(e):\n        raise UnsafeSkillNameError(skill_name) from e\n    raise","preventionTips":["Validate skill names against a strict allowlist pattern before downloading.","Never pass user- or remote-supplied names directly into download_skill without sanitizing.","Treat traversal attempts as a security signal — reject and log, don't normalize."],"tags":["security","path-traversal","skills"],"backgroundTag":"path-traversal-detected","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}