github/spec-kit · error · SystemExit

Error: branch_template must not place {slug} before {number}

Error message

Error: branch_template must not place {slug} before {number}; use {slug} only in the final feature segment.

What it means

After mkdir() creates a directory component, _ensure_safe_shared_directory() re-checks is_symlink(). This catches the race where an attacker (or concurrent process) replaces the just-created directory with a symlink between creation and check — a classic TOCTOU mitigation. It is the same SymlinkedSharedPathError as the pre-check, raised from the post-creation branch.

Source

Thrown at extensions/git/scripts/python/create_new_feature_branch.py:381

    return f"{prefix}/{{number}}-{{slug}}"


def validate_branch_template(template: str) -> None:
    if not template:
        return
    if "{number}" not in template:
        _err(
            "Error: branch_template must include the {number} token so generated "
            "branches remain valid feature branches."
        )
        raise SystemExit(1)
    slug_index = template.find("{slug}")
    if slug_index != -1 and "{number}" in template[slug_index:]:
        _err(
            "Error: branch_template must not place {slug} before {number}; "
            "use {slug} only in the final feature segment."
        )
        raise SystemExit(1)
    feature_segment = template.rsplit("/", 1)[-1]
    if not feature_segment.startswith("{number}-"):
        _err(
            "Error: branch_template must put {number}- at the start of the final "
            "path segment so generated branches remain valid feature branches."
        )
        raise SystemExit(1)


def render_branch_template(
    template: str, feature_num: str, branch_suffix: str, author_token: str, app_token: str
) -> str:
    rendered = template
    rendered = rendered.replace("{author}", author_token)
    rendered = rendered.replace("{app}", app_token)
    rendered = rendered.replace("{number}", feature_num)
    rendered = rendered.replace("{slug}", branch_suffix)
    return rendered

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Retry the operation once from a clean state (remove the swapped path and rerun) — genuine races are transient
  2. Restrict write access to the project tree (chmod o-w) so other users cannot swap components
  3. Disable or exclude the project directory from sync/watch tools that rewrite paths during installs
  4. If it reproduces deterministically, inspect which component is a symlink after failure and remove it

Example fix

# before: race during install raises SymlinkedSharedPathError

# after: retry once after cleaning the offending component
try:
    install()
except SymlinkedSharedPathError as e:
    remove_symlinked_component(e)  # unlink the swapped component
    install()
Defensive patterns

Strategy: retry

Try / catch

from specify_cli.shared_infra import SymlinkedSharedPathError

for attempt in range(2):
    try:
        install_shared(project_path)
        break
    except SymlinkedSharedPathError as e:
        if attempt:
            raise
        remove_offending_symlink_from(e)  # unlink component named in message

Prevention

When it happens

Trigger: A concurrent process swaps the freshly created directory for a symlink before the is_symlink() check; a malicious environment where an automated watcher re-links paths as they appear; tests that simulate races with threads manipulating the tree during install.

Common situations: Malicious multi-user environments where the project tree is writable by others; overly aggressive file watchers/sync tools (Dropbox, syncthing) racing with the install; security test suites deliberately racing the guard.

Related errors


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