github/spec-kit · error · ValueError
Refusing to record symlinked manifest path: {rel} (symlinked
Error message
Refusing to record symlinked manifest path: {rel} (symlinked at {_walk.relative_to(self.project_root).as_posix()}) What it means
Raised when walking the components of a rel_path passed to record_file()/record_existing(): some ancestor directory of the file is a symlink. Recording it would let resolve() silently follow the link, so the file could be tracked (and later uninstalled) outside the project root.
Source
Thrown at src/specify_cli/integrations/manifest.py:215
_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():
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)View on GitHub (pinned to bf88c9f9a8)
Solutions
- Replace the symlinked ancestor with a real directory containing the file
- If sharing is needed, copy files into the project before recording them
- Skip recording linked paths; manage those files outside the manifest
Example fix
# before
.claude/commands -> ../../shared/commands
manifest.record_file(".claude/commands/build.md")
# after
cp -r ../../shared/commands/. .claude/commands/
manifest.record_file(".claude/commands/build.md") Defensive patterns
Strategy: validation
Validate before calling
def has_symlinked_ancestor(root: Path, rel: Path) -> bool:
cur = root
for part in rel.parts:
cur = cur / part
if cur.is_symlink():
return True
return False
if not has_symlinked_ancestor(project_root, rel):
manifest.record_file(rel) Try / catch
try:
manifest.record_file(rel)
except ValueError as exc:
if "symlinked manifest path" in str(exc):
copy_file_into_project(rel) # then record the copied path
else:
raise Prevention
- Materialize shared command dirs into the repo instead of symlinking
- Audit monorepo setups for linked agent config dirs
- Exclude manifest-tracked trees from dotfile managers
When it happens
Trigger: record_file('linked_dir/cmd.md') where linked_dir is a symlink inside the project pointing elsewhere (inside or outside); the check is unconditional on the ancestor being a link, regardless of target.
Common situations: Monorepos symlinking shared command directories into per-package agent dirs; dotfile-managed .claude/.kilo directories; worktrees sharing command folders via links.
Related errors
- Refusing to use symlinked integration manifest directory: {l
- Refusing to overwrite symlinked integration manifest path: {
- Error: --short-name requires a value
- Error: --number must be a non-negative integer
- Error: branch_template must not place {slug} before {number}
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/2e5d734cc2f86885.
Report an issue: GitHub.