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 renderedView on GitHub (pinned to bf88c9f9a8)
Solutions
- Retry the operation once from a clean state (remove the swapped path and rerun) — genuine races are transient
- Restrict write access to the project tree (chmod o-w) so other users cannot swap components
- Disable or exclude the project directory from sync/watch tools that rewrite paths during installs
- 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
- Restrict write permissions on the project tree in shared/multi-user machines (chmod o-w)
- Exclude the repo from sync/watch tools that rewrite paths during installs
- Treat repeat occurrences as a compromised or racing environment, not a CLI bug
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
- Error: --short-name requires a value
- Error: --number must be a non-negative integer
- Error: branch_template must put {number}- at the start of th
- 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/14f47b8e1e0be584.
Report an issue: GitHub.