agentscope-ai/agentscope · error · ValueError
Skill path {skill_path!r} resolves outside skills_dir.
Error message
Skill path {skill_path!r} resolves outside skills_dir. What it means
Raised by LocalWorkspace.add_skill when the computed destination path inside skills_dir resolves (via realpath) outside the skills directory itself. This is a path-traversal guard: the skill's directory name, after symlink resolution, must stay within skills_dir + os.sep. It protects the agent's skill partition from escapes via symlinks or crafted directory names.
Source
Thrown at src/agentscope/workspace/_local_workspace.py:841
counter = 1
while agent_name in existing_agent_names:
agent_name = f"{raw_name} ({counter})"
counter += 1
# Resolve directory name conflict
base_dir = _sanitize_dir_name(raw_name)
dir_name = base_dir
counter = 1
while dir_name in existing_dir_names:
dir_name = f"{base_dir}_{counter}"
counter += 1
dest_path = os.path.join(skills_dir, dir_name)
if not os.path.realpath(dest_path).startswith(
os.path.realpath(skills_dir) + os.sep,
):
raise ValueError(
f"Skill path {skill_path!r} resolves outside skills_dir.",
)
await asyncio.to_thread(
shutil.copytree,
skill_path,
dest_path,
dirs_exist_ok=False,
)
logger.info(
"Copied skill '%s' (agent name: '%s') from %s to %s",
raw_name,
agent_name,
skill_path,
dest_path,
)
View on GitHub (pinned to e90f1c7592)
Solutions
- Pass the canonical skills_dir: use os.path.realpath on the configured skills directory before constructing the workspace.
- Ensure the skill source directory is a real directory, not a symlink pointing outside skills_dir.
- Check dir_name derived from the skill for path separators or '..' and rename the skill folder to a plain directory name.
- Re-run add_skill after fixing so realpath(dest) starts with realpath(skills_dir) + os.sep.
Example fix
# before
ws = LocalWorkspace(skills_dir="/tmp/skills") # /tmp -> /private/tmp symlink
await ws.add_skill("./my-skill")
# after
import os
ws = LocalWorkspace(skills_dir=os.path.realpath("/tmp/skills"))
await ws.add_skill("./my-skill") Defensive patterns
Strategy: validation
Validate before calling
import os
def skill_dest_is_safe(skills_dir: str, skill_path: str) -> bool:
root = os.path.realpath(skills_dir)
dest = os.path.realpath(os.path.join(skills_dir, os.path.basename(skill_path.rstrip("/"))))
return dest.startswith(root + os.sep) Try / catch
try:
await ws.add_skill(path)
except ValueError as e:
if "resolves outside skills_dir" in str(e):
raise ValueError(f"symlinked skill dir not allowed: {path}") from e
raise Prevention
- Canonicalize skills_dir with os.path.realpath before configuring the workspace.
- Avoid symlinking individual skill directories; copy them instead.
- Keep skill folder names simple identifiers (letters, digits, dashes).
When it happens
Trigger: Calling add_skill with a skill directory whose name resolves oddly (e.g. '..'-containing names), or when skills_dir itself is (or contains) a symlink such that realpath(dest_path) no longer shares the realpath(skills_dir) prefix; also a skills_dir that resolves to the filesystem root.
Common situations: Skills directory placed under a symlinked path (e.g. /tmp on macOS symlinked to /private/tmp) where skills_dir passed in is not the realpath, or a skill folder that is itself a symlink pointing elsewhere.
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
- Invalid skill at {skill_path!r}: missing or malformed SKILL.
- Blob key {key!r} escapes the root directory.
- Bubblewrap workdir escapes basedir.
- workspace_id {workspace_id!r} escapes the workspace base dir
- unsafe tar member: ' + m.name
AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28).
Data as JSON: /api/errors/cc4b7d9fe6f3254b.
Report an issue: GitHub.