{"record":{"id":"14f47b8e1e0be584","repo":"github/spec-kit","slug":"error-branch-template-must-not-place-slug-befor","errorCode":null,"errorMessage":"Error: branch_template must not place {slug} before {number}; use {slug} only in the final feature segment.","messagePattern":"Error: branch_template must not place (.+?) before (.+?); use (.+?) only in the final feature segment\\.","errorType":"console","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"extensions/git/scripts/python/create_new_feature_branch.py","lineNumber":381,"sourceCode":"    return f\"{prefix}/{{number}}-{{slug}}\"\n\n\ndef validate_branch_template(template: str) -> None:\n    if not template:\n        return\n    if \"{number}\" not in template:\n        _err(\n            \"Error: branch_template must include the {number} token so generated \"\n            \"branches remain valid feature branches.\"\n        )\n        raise SystemExit(1)\n    slug_index = template.find(\"{slug}\")\n    if slug_index != -1 and \"{number}\" in template[slug_index:]:\n        _err(\n            \"Error: branch_template must not place {slug} before {number}; \"\n            \"use {slug} only in the final feature segment.\"\n        )\n        raise SystemExit(1)\n    feature_segment = template.rsplit(\"/\", 1)[-1]\n    if not feature_segment.startswith(\"{number}-\"):\n        _err(\n            \"Error: branch_template must put {number}- at the start of the final \"\n            \"path segment so generated branches remain valid feature branches.\"\n        )\n        raise SystemExit(1)\n\n\ndef render_branch_template(\n    template: str, feature_num: str, branch_suffix: str, author_token: str, app_token: str\n) -> str:\n    rendered = template\n    rendered = rendered.replace(\"{author}\", author_token)\n    rendered = rendered.replace(\"{app}\", app_token)\n    rendered = rendered.replace(\"{number}\", feature_num)\n    rendered = rendered.replace(\"{slug}\", branch_suffix)\n    return rendered","sourceCodeStart":363,"sourceCodeEnd":399,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/extensions/git/scripts/python/create_new_feature_branch.py#L363-L399","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before: race during install raises SymlinkedSharedPathError\n\n# after: retry once after cleaning the offending component\ntry:\n    install()\nexcept SymlinkedSharedPathError as e:\n    remove_symlinked_component(e)  # unlink the swapped component\n    install()","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from specify_cli.shared_infra import SymlinkedSharedPathError\n\nfor attempt in range(2):\n    try:\n        install_shared(project_path)\n        break\n    except SymlinkedSharedPathError as e:\n        if attempt:\n            raise\n        remove_offending_symlink_from(e)  # unlink component named in message","preventionTips":["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"],"tags":["filesystem","symlink","race-condition","security"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}