github/spec-kit · error · ValueError

Manifest paths must be canonical; '..' segments are not allo

Error message

Manifest paths must be canonical; '..' segments are not allowed (got {rel})

What it means

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.

Source

Thrown at src/specify_cli/integrations/manifest.py:202

            OSError: if the underlying filesystem call (``is_symlink``,
                ``is_file``, or the file-read used to compute the hash)
                fails — for example a ``PermissionError`` on the path.
                Callers should be prepared to handle ``OSError`` (and its
                subclasses such as ``PermissionError``) in addition to
                ``ValueError``.
        """
        rel = Path(rel_path)
        # Cheap lexical pre-check first so absolute / parent-traversal paths
        # don't trigger a filesystem stat outside the project root before
        # ``_validate_rel_path`` raises. ``_validate_rel_path`` produces the
        # canonical error messages used elsewhere.
        if rel.is_absolute() or ".." in rel.parts:
            _validate_rel_path(rel, self.project_root)
            # _validate_rel_path raised for any actually-escaping path. If we reach
            # here the path normalizes inside root (e.g. ``dir/../file.txt``).
            # Reject anyway: manifest keys must be canonical so ``check_modified``
            # and ``uninstall`` cannot key the same file under two paths.
            raise ValueError(
                f"Manifest paths must be canonical; '..' segments are not "
                f"allowed (got {rel})"
            )
        # Walk each path component before resolution so a symlinked ancestor
        # (e.g. ``linked_dir/file.txt`` where ``linked_dir`` is a symlink)
        # cannot be silently followed by ``_validate_rel_path().resolve()``
        # down to a target outside the project root. ``_ensure_safe_manifest_directory``
        # uses the same pattern.
        _walk = self.project_root
        for part in rel.parts:
            _walk = _walk / part
            if _walk.is_symlink():
                raise ValueError(
                    f"Refusing to record symlinked manifest path: {rel} "
                    f"(symlinked at {_walk.relative_to(self.project_root).as_posix()})"
                )
        abs_path = _validate_rel_path(rel, self.project_root)
        if not abs_path.is_file():

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Normalize and relativize the path before recording: rel = Path(os.path.normpath(rel)) and strip any '..' that remain
  2. Compute the path relative to project_root with abs_path.relative_to(project_root) instead of string surgery
  3. Reject '..' paths at the boundary of your own code that feeds the manifest

Example fix

// before
manifest.record_file(".claude/commands/../commands/build.md")
// after
manifest.record_file(".claude/commands/build.md")
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
import os

rel = Path(os.path.normpath(str(rel)))
if rel.is_absolute() or ".." in rel.parts:
    rel = Path.cwd().joinpath(rel).resolve().relative_to(project_root)
manifest.record_file(rel.as_posix())

Type guard

def is_canonical_manifest_key(rel: str) -> bool:
    p = pathlib.PurePosixPath(rel)
    return bool(rel) and not p.is_absolute() and ".." not in p.parts

Try / catch

try:
    manifest.record_file(rel)
except ValueError as exc:
    if "must be canonical" in str(exc):
        manifest.record_file(Path(os.path.normpath(rel)).as_posix())
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/ddfc9b5c6ff76a95. Report an issue: GitHub.