github/spec-kit · error · ValueError

Manifest path is not a regular file: {rel}

Error message

Manifest path is not a regular file: {rel}

What it means

Raised by record_file()/record_existing() after validation passes but abs_path.is_file() is false — the path does not exist or is not a regular file (directory, FIFO, broken link already rejected earlier). The manifest records SHA-256 hashes, so it can only track real files that exist at record time.

Source

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

                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():
            raise ValueError(
                f"Manifest path is not a regular file: {rel}"
            )
        normalized = abs_path.relative_to(self.project_root).as_posix()
        self._files[normalized] = _sha256(abs_path)
        if recovered:
            self._recovered_files.add(normalized)
        else:
            # ``recovered=False`` means the caller is asserting this path is
            # managed-baseline now, not merely observed; drop any stale
            # recovered marker so future is_recovered() queries reflect the
            # transition. ``discard`` is a no-op when the key is absent.
            self._recovered_files.discard(normalized)

    def remove(self, rel_path: str | Path) -> bool:
        """Drop *rel_path* from the tracked file set and any recovered marker.

        Operates purely on the manifest's recorded key; it does NOT touch the
        file on disk. Returns ``True`` if an entry was present and removed.

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Verify ordering: create/write the file first, then manifest.record_file(rel)
  2. Check the path exists and is a file before recording (see validation snippet)
  3. If the file legitimately may be absent, skip recording instead of erroring

Example fix

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

Strategy: validation

Validate before calling

target = project_root / rel
if not target.is_file():
    raise RuntimeError(f"file not written yet: {rel}")
manifest.record_file(rel)

Try / catch

try:
    manifest.record_file(rel)
except ValueError as exc:
    if "not a regular file" in str(exc):
        write_file_then_record(rel)  # fix ordering, retry once
    else:
        raise

Prevention

When it happens

Trigger: Calling record_file() before the file was written (ordering bug in custom setup() logic), recording a path that is a directory, or a TOCTOU deletion between validation and the is_file() check.

Common situations: Custom integrations whose setup() writes files after calling record_file(); template rendering that silently failed; typos in the rel_path so the intended file never matched.

Related errors


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