github/spec-kit · error · SystemExit

Error: branch_template must put {number}- at the start of th

Error message

Error: branch_template must put {number}- at the start of the final path segment so generated branches remain valid feature branches.

What it means

The post-creation branch of _ensure_safe_shared_directory(): after mkdir(), resolve() of the new directory does not stay under the resolved project root. Like error 464's pre-check, this guards against mounts/junctions that materialize outside the root, but here it fires after creation because the resolution only becomes visible once the directory exists.

Source

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

        _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


def extract_feature_num_from_branch(branch_name: str) -> str:
    feature_segment = branch_name.rsplit("/", 1)[-1]
    match = re.match(r"^[0-9]{8}-[0-9]{6}-", feature_segment)
    if match:
        return match.group(0).rstrip("-")

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Always pass project_path as Path(...).resolve() so the root matches what resolve() will produce for children
  2. Work in a native, non-redirecting directory (e.g. ~/work/proj instead of /tmp/proj on macOS)
  3. Avoid FUSE/mount-backed paths inside the project tree for .specify
  4. If reproducible only in CI, compare Path(p).resolve() for root and children in a debug step to find the redirect

Example fix

# before
run(project_path=Path('/tmp/proj'))

# after
run(project_path=Path('/tmp/proj').resolve())  # consistent with internal root resolution
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

root = project_path.resolve()
# simulate what ensure will do after creation
assert (root / rel_dir).resolve().is_relative_to(root) or not (root / rel_dir).exists()

Try / catch

try:
    _ensure_safe_shared_directory(project_path, directory, create=True)
except ValueError as e:
    if 'escapes project root' in str(e):
        _ensure_safe_shared_directory(Path(project_path).resolve(), directory, create=True)
    else:
        raise

Prevention

When it happens

Trigger: The created directory sits on a mount whose resolution escapes root (autofs, FUSE, bind mounts); project_path passed unresolved while creation triggers a mount/symlink resolution (e.g. macOS /tmp vs /private/tmp autofs); container runtimes mounting over just-created paths.

Common situations: Running installs under /tmp or /var/folders on macOS (symlinked to /private/...); FUSE filesystems (sshfs, gcsfuse) inside the repo; sidecar containers that bind-mount into the workspace on file creation; CI with overlay filesystem quirks.

Related errors


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