{"record":{"id":"946da802f26210e5","repo":"gitbutlerapp/gitbutler","slug":"cannot-delete-gitbutler-technical-branch","errorCode":null,"errorMessage":"Cannot delete GitButler technical branch '{}'","messagePattern":"Cannot delete GitButler technical branch '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-api/src/branch.rs","lineNumber":1117,"sourceCode":"/// Remove the local branch `ref_name`, whether or not it is part of the current\n/// workspace projection, under caller-held exclusive repository access and\n/// record an oplog snapshot on success.\n///\n/// See [`branch_remove()`] for the checked-out-reference behaviour and\n/// [`but_workspace::branch::remove_reference()`] for the lower-level deletion.\npub fn branch_remove_with_perm(\n    ctx: &mut but_ctx::Context,\n    ref_name: gix::refs::FullName,\n    perm: &mut RepoExclusive,\n) -> anyhow::Result<BranchRemoveResult> {\n    if ref_name.category() != Some(gix::refs::Category::LocalBranch) {\n        bail!(\n            \"Can only delete local branches under refs/heads, got '{}'\",\n            ref_name.as_bstr()\n        );\n    }\n    if but_branches::is_technical_branch_name(ref_name.shorten()) {\n        bail!(\n            \"Cannot delete GitButler technical branch '{}'\",\n            ref_name.shorten()\n        );\n    }\n\n    let maybe_oplog_entry = but_oplog::UnmaterializedOplogSnapshot::from_details_with_perm(\n        ctx,\n        SnapshotDetails::new(OperationKind::DeleteBranch)\n            .with_trailers([Trailer::Name(ref_name.to_string())]),\n        perm.read_permission(),\n        DryRun::No,\n    );\n\n    // Decide whether we must move `HEAD` off `ref_name` before deleting it. In an\n    // ad-hoc workspace the checked-out reference is the projection tip; we only\n    // allow removing it when it owns no commits and has another named reference\n    // underneath to land `HEAD` on. This reverses the \"create an empty branch\n    // above the checked-out reference\" flow. We look at the projection rather","sourceCodeStart":1099,"sourceCodeEnd":1135,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-api/src/branch.rs#L1099-L1135","documentation":"`branch_remove_with_perm` protects GitButler's internal technical branches (e.g. `gitbutler/workspace`, oplog/snapshot refs) from deletion. If the shortened refname is classified as a technical branch, deletion is refused because removing it would corrupt workspace state.","triggerScenarios":"Attempting to delete a branch whose name matches `but_branches::is_technical_branch_name` — typically `gitbutler/workspace` or other `gitbutler/*` internal refs — through the branch-delete API.","commonSituations":"Scripted cleanup deleting all `refs/heads/*` branches and hitting GitButler internals; users trying to remove the workspace branch manually; bulk deletes from tooling unaware of GitButler's reserved names.","solutions":["Exclude technical branch names (check `but_branches::is_technical_branch_name`) from deletion logic","Operate on user branches only; let GitButler manage its own internal refs","If the workspace itself should go away, use the workspace/unassign or project-removal flows instead of deleting the technical branch"],"exampleFix":"// before\nfor b in branches { repo.delete_branch(b)?; }\n// after\nfor b in branches {\n  if !is_technical_branch_name(b) {\n    repo.delete_branch(b)?;\n  }\n}","handlingStrategy":"validation","validationCode":"const TECHNICAL = /^(gitbutler\\/)/;\nif (TECHNICAL.test(shortName)) throw new Error(`refusing to delete GitButler technical branch: ${shortName}`);","typeGuard":"function isTechnicalBranch(shortName: string): boolean {\n  return shortName === \"gitbutler/workspace\" || shortName.startsWith(\"gitbutler/\");\n}","tryCatchPattern":"try {\n  await api.branchRemove(ref);\n} catch (e) {\n  if (String(e).includes(\"Cannot delete GitButler technical branch\")) {\n    console.warn(\"skipping GitButler-internal branch\");\n  } else { throw e; }\n}","preventionTips":["Exclude gitbutler/* names from any bulk branch cleanup","Treat GitButler internal refs as owned by the tool","Use workspace/unassign flows instead of deleting the workspace branch","Check is_technical_branch_name before scripting deletions"],"tags":["git","branch","protected-ref","gitbutler"],"backgroundTag":"unsupported-operation","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}