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
- 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.
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
- 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.
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
- Invalid path: {e}
- The CLI state lock must not be a symbolic link
- CLI state must not be a symbolic link: {path.name}
- {str(e) from SSRFError/SSRFFetchError}
- CIMD jwks_uri failed SSRF validation: {e}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/59d78157b9eb7ed3.
Report an issue: GitHub.