github/spec-kit · error · ValueError
Refusing to write event config through a symlink: {walked}
Error message
Refusing to write event config through a symlink: {walked} What it means
A deliberate security guard: Specify refuses to write generated event configuration to a destination path that passes through a symbolic link, because a symlinked `.claude` or `.specify` directory could redirect writes to files outside the repository. Each path component is walked and checked with is_symlink(); a lexical containment check against the nearest existing ancestor then rejects `..` traversal as well.
Source
Thrown at src/specify_cli/events.py:2456
def _ensure_safe_destination(dst: Path) -> None:
"""Validate a write target is a regular path inside the project (#12).
Walks each path component and rejects symlinks (which could escape the
project — e.g. a symlinked ``.claude`` or ``.specify`` directory pointing
outside the repo would redirect writes to external files). Then validates
lexical containment so ``..`` traversal is also rejected.
"""
from .agents import CommandRegistrar
# Walk each component so a symlinked ancestor (e.g. ``.claude`` → outside)
# cannot be silently followed. Mirrors IntegrationManifest.record_existing.
walked = dst.anchor and Path(dst.anchor) or Path("/")
for part in dst.relative_to(dst.anchor).parts if dst.anchor else dst.parts:
walked = walked / part
if walked.is_symlink():
raise ValueError(
f"Refusing to write event config through a symlink: {walked}"
)
# Containment check against the nearest existing ancestor directory.
base = dst.parent
while not base.exists() and base != base.parent:
base = base.parent
CommandRegistrar._ensure_inside(dst, base)
def _remove_json_entries(dst: Path) -> bool:
"""Remove Specify-authored entries; delete the file if now empty (#14).
Returns True if the file was deleted (Spec Kit created it and no user
content remains), False otherwise.
"""
_ensure_safe_destination(dst)
existing = _load_user_json(dst)View on GitHub (pinned to bf88c9f9a8)
Solutions
- Replace the symlinked directory with a real directory and copy (or manage) its contents directly.
- If you need shared config, have Specify write in-repo and symlink individual non-managed files yourself — never a directory Specify writes into.
- Check every component of the destination path from repo root down (`ls -la` each level) to find the link named in the error.
- Do not attempt to work around this — the redirect-outside-repo risk is exactly what the guard prevents.
Example fix
# before ln -s ~/dotfiles/claude .claude specify extension install my-ext # ValueError: symlink # after mkdir .claude cp ~/dotfiles/claude/* .claude/ # manage contents directly specify extension install my-ext
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def path_crosses_symlink(dst: Path) -> bool:
walked = Path(dst.anchor or "/")
for part in dst.relative_to(dst.anchor).parts:
walked = walked / part
if walked.is_symlink():
return True
return False
if path_crosses_symlink(config_dst):
raise SystemExit("replace symlinked config dir with a real directory") Try / catch
try:
install_integration_events(integration, root, manifest, events_map)
except ValueError as e:
if "symlink" in str(e):
materialize_dir(e_path) # copy link target contents into a real dir, then retry Prevention
- Never symlink `.claude`, `.specify`, or other directories Specify writes into.
- With dotfile managers, exclude agent config dirs from stow/chezmoi linking.
- Check `ls -la` for stray links after repo scaffolding in CI.
When it happens
Trigger: install_integration_events computes a destination (e.g. `.claude/settings.json` or a config path under `.specify`) where any ancestor component — `.claude`, `.specify`, or a parent — is a symlink; the write is aborted with ValueError naming the offending component.
Common situations: Developers symlinking dotfile directories between repos or to a shared dotfiles repo; a monorepo setup where `.claude` is a link to a central config dir; CI environments that link config dirs into the workspace; dotfiles managers (stow, chezmoi) creating links.
Related errors
- Error: --short-name requires a value
- Error: --number must be a non-negative integer
- Error: branch_template must not place {slug} before {number}
- ERROR: --template requires a template name
- ERROR: SPECIFY_INIT_DIR does not point to an existing direct
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/5cbaea56899a1d37.
Report an issue: GitHub.