{"record":{"id":"5c8e1cffcd87847d","repo":"bmad-code-org/BMAD-METHOD","slug":"render-source-escapes-skill-directory-name","errorCode":null,"errorMessage":"render source escapes skill directory: {name}","messagePattern":"render source escapes skill directory: (.+?)","errorType":"exception","errorClass":"RenderError","httpStatus":null,"severity":"critical","filePath":"src/scripts/render_skill.py","lineNumber":106,"sourceCode":"            \"instruction\": _require_string(\n                item.get(\"instruction\"), f\"{item_label}.instruction\", allow_empty=True\n            ),\n        }\n        if \"when\" in item:\n            layer[\"when\"] = _require_string(item[\"when\"], f\"{item_label}.when\")\n        result.append(layer)\n    return result\n\n\ndef _load_sources(skill_dir: Path) -> dict[str, str]:\n    sources: dict[str, str] = {}\n    for candidate in sorted(skill_dir.rglob(\"*.md\")):\n        if candidate.name == \"SKILL.md\":\n            continue\n        name = candidate.relative_to(skill_dir).as_posix()\n        path = candidate.resolve(strict=True)\n        if not path.is_relative_to(skill_dir):\n            raise RenderError(f\"render source escapes skill directory: {name}\")\n        if not path.is_file():\n            raise RenderError(f\"render source is missing or not a file: {path}\")\n        try:\n            sources[name] = path.read_text(encoding=\"utf-8\")\n        except (OSError, UnicodeError) as error:\n            raise RenderError(f\"failed to read render source {path}: {error}\") from error\n    if \"workflow.md\" not in sources:\n        raise RenderError(f\"render entry is missing: {skill_dir / 'workflow.md'}\")\n    return sources\n\n\ndef _resolve_config_value(value: Any, label: str, project_root: Path) -> str:\n    text = _require_string(value, label)\n    if \"{project-root}\" not in text:\n        return text\n    resolved = text.replace(\"{project-root}\", str(project_root))\n    if not Path(resolved).is_absolute():\n        raise RenderError(f\"{label} must resolve to an absolute path: {resolved}\")","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/scripts/render_skill.py#L88-L124","documentation":"Security guard in _load_sources: after resolving each *.md candidate (following symlinks via Path.resolve(strict=True)), the renderer verifies the real path stays inside skill_dir. A symlink that resolves outside the skill directory is rejected to prevent path traversal and arbitrary external content being baked into the immutable snapshot.","triggerScenarios":"A symlinked .md file inside the skill directory whose target resolves outside skill_dir.resolve() -- e.g. ln -s /etc/passwd skills/my-skill/notes.md, or a directory symlink creating an escape. path.is_relative_to(skill_dir) returns False.","commonSituations":"Symlinking shared docs from another skill or repo; developer symlinks into /tmp or $HOME; monorepo cross-package symlinks; bundling skills via symlink farms.","solutions":["Replace the offending symlink with a real file (copy) inside the skill directory.","Repoint the symlink so its target resolves to a path inside the skill directory.","Avoid symlinks for skill sources; keep all .md files physical within the skill dir."],"exampleFix":"# before\nln -s ../../shared/notes.md skills/my-skill/notes.md\n\n# after\ncp ../../shared/notes.md skills/my-skill/notes.md\n# or keep the symlink target inside the skill directory","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef assert_sources_inside(skill_dir: Path) -> None:\n    skill_dir = skill_dir.resolve()\n    for cand in skill_dir.rglob(\"*.md\"):\n        if cand.name == \"SKILL.md\":\n            continue\n        real = cand.resolve(strict=True)\n        if not real.is_relative_to(skill_dir):\n            raise SystemExit(f\"source escapes skill dir: {cand}\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Avoid symlinks for skill sources; keep .md files physical inside the skill dir.","Audit symlinks with find <skill_dir> -type l -name '*.md' before rendering.","If sharing content, copy it in rather than symlinking across package boundaries."],"tags":["python","security","path-traversal","symlink","filesystem","skill"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}