{"record":{"id":"ddfc9b5c6ff76a95","repo":"github/spec-kit","slug":"manifest-paths-must-be-canonical-segments-ar","errorCode":null,"errorMessage":"Manifest paths must be canonical; '..' segments are not allowed (got {rel})","messagePattern":"Manifest paths must be canonical; '\\.\\.' segments are not allowed \\(got (.+?)\\)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/manifest.py","lineNumber":202,"sourceCode":"            OSError: if the underlying filesystem call (``is_symlink``,\n                ``is_file``, or the file-read used to compute the hash)\n                fails — for example a ``PermissionError`` on the path.\n                Callers should be prepared to handle ``OSError`` (and its\n                subclasses such as ``PermissionError``) in addition to\n                ``ValueError``.\n        \"\"\"\n        rel = Path(rel_path)\n        # Cheap lexical pre-check first so absolute / parent-traversal paths\n        # don't trigger a filesystem stat outside the project root before\n        # ``_validate_rel_path`` raises. ``_validate_rel_path`` produces the\n        # canonical error messages used elsewhere.\n        if rel.is_absolute() or \"..\" in rel.parts:\n            _validate_rel_path(rel, self.project_root)\n            # _validate_rel_path raised for any actually-escaping path. If we reach\n            # here the path normalizes inside root (e.g. ``dir/../file.txt``).\n            # Reject anyway: manifest keys must be canonical so ``check_modified``\n            # and ``uninstall`` cannot key the same file under two paths.\n            raise ValueError(\n                f\"Manifest paths must be canonical; '..' segments are not \"\n                f\"allowed (got {rel})\"\n            )\n        # Walk each path component before resolution so a symlinked ancestor\n        # (e.g. ``linked_dir/file.txt`` where ``linked_dir`` is a symlink)\n        # cannot be silently followed by ``_validate_rel_path().resolve()``\n        # down to a target outside the project root. ``_ensure_safe_manifest_directory``\n        # uses the same pattern.\n        _walk = self.project_root\n        for part in rel.parts:\n            _walk = _walk / part\n            if _walk.is_symlink():\n                raise ValueError(\n                    f\"Refusing to record symlinked manifest path: {rel} \"\n                    f\"(symlinked at {_walk.relative_to(self.project_root).as_posix()})\"\n                )\n        abs_path = _validate_rel_path(rel, self.project_root)\n        if not abs_path.is_file():","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/manifest.py#L184-L220","documentation":"Raised by record_file()/record_existing() when the rel_path contains '..' segments, even if they would normalize back inside the project (e.g. 'dir/../file.txt'). Manifest keys must be canonical so check_modified() and uninstall() cannot address the same file under two different paths.","triggerScenarios":"Calling manifest.record_file('a/../b/cmd.md') or record_existing('.claude/commands/../commands/x.md'); the lexical pre-check sees '..' in rel.parts, delegates escaping paths to _validate_rel_path, and rejects the rest with this message.","commonSituations":"Paths assembled from template names that include '..' (e.g. '../shared/'); porting code that used os.path.join without normalizing; copy-pasted absolute paths converted to relative by stripping a prefix.","solutions":["Normalize and relativize the path before recording: rel = Path(os.path.normpath(rel)) and strip any '..' that remain","Compute the path relative to project_root with abs_path.relative_to(project_root) instead of string surgery","Reject '..' paths at the boundary of your own code that feeds the manifest"],"exampleFix":"// before\nmanifest.record_file(\".claude/commands/../commands/build.md\")\n// after\nmanifest.record_file(\".claude/commands/build.md\")","handlingStrategy":"validation","validationCode":"from pathlib import Path\nimport os\n\nrel = Path(os.path.normpath(str(rel)))\nif rel.is_absolute() or \"..\" in rel.parts:\n    rel = Path.cwd().joinpath(rel).resolve().relative_to(project_root)\nmanifest.record_file(rel.as_posix())","typeGuard":"def is_canonical_manifest_key(rel: str) -> bool:\n    p = pathlib.PurePosixPath(rel)\n    return bool(rel) and not p.is_absolute() and \"..\" not in p.parts","tryCatchPattern":"try:\n    manifest.record_file(rel)\nexcept ValueError as exc:\n    if \"must be canonical\" in str(exc):\n        manifest.record_file(Path(os.path.normpath(rel)).as_posix())\n    else:\n        raise","preventionTips":["Always derive keys via abs.relative_to(project_root)","Normalize paths at your API boundary before passing them in","Add a lint check banning '..' in recorded manifest paths"],"tags":["manifest","canonicalization","path-traversal"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}