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

  1. Replace the symlinked directory with a real directory and copy (or manage) its contents directly.
  2. If you need shared config, have Specify write in-repo and symlink individual non-managed files yourself — never a directory Specify writes into.
  3. Check every component of the destination path from repo root down (`ls -la` each level) to find the link named in the error.
  4. 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

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


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