You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Guard worktree removal or branch cleanup with an expected branch OID #3700
An orchestrator may need Worktrunk to run pre/post-remove hooks and remove a worktree while retaining responsibility for branch identity. Between preflight and deletion, a branch name can be reused or advanced.
Request
Add an optional expected branch OID token to removal or a safe branch-scoped cleanup operation. Worktrunk should fail closed if the branch no longer resolves to that OID.
This would permit atomic lifecycle cleanup without allowing --force to delete a changed branch and without requiring broad Worktrunk state cleanup.
Thanks for the suggestion — this fits Worktrunk's fail-closed data-safety posture well, and part of the mechanism you're asking for already exists internally.
wt remove already deletes the branch with a compare-and-swap, not a plain git branch -d: delete_branch_if_safe snapshots the branch tip, then cas_delete_branch_outcome runs git update-ref -d <ref> <expected-oid>, which fails closed (RetainedRaced, branch kept) if a hook or concurrent process advanced the branch in the meantime.
The gap is exactly the one you describe: that expected OID is captured by Worktrunk at its own snapshot, so the guard only covers Worktrunk's internal window (snapshot → delete) — not your orchestrator's window (preflight → the moment wt remove starts). Between those, a branch could be reused or advanced and Worktrunk would delete against its own fresh read without knowing your expectation was already violated. And the --force / -D path skips the CAS entirely (branch -D, no expected value), which is the "changed branch deleted under --force" case you're calling out.
So a caller-supplied token would slot in cleanly: an optional expected_oid on RemoveArgs threaded down to the expected_sha argument of cas_delete_branch_outcome, extending the existing CAS back to your observed OID and, unlike the internal snapshot, also applying on the force path. The primitive is already there; what's missing is the external interface to feed it a caller-chosen value.
I'll leave it to a maintainer to weigh the exact surface (a new --expected-oid <sha> flag vs. a branch-scoped cleanup subcommand) and prioritize.
Thanks — the fail-closed posture here is right, and I want to check how much of the window actually bites you before adding a flag for it.
Where things stand: #3710 makes the guard's outcome visible. wt remove --format=json reported the branch as branch_deleted: bool, which collapsed a CAS refusal into the same false as --no-delete-branch. It now reports branch_outcome, one of deleted, deferred, not_attempted, retained_unmerged, retained_checked_out, retained_raced, retained_failed. So a caller can already detect the race you describe and retry, where before it couldn't tell that case apart. That's an output, not the input this issue asks for — it doesn't close the request.
On the input: git's primitive is there and Worktrunk already calls it (git update-ref -d <ref> <old-oid>, which deletes only if the ref still holds <old-oid>), so the mechanism isn't the obstacle. What I'd like to understand before deciding is which window is actually costing you something.
Is the concern the default path or the force path? On the default path Worktrunk won't delete a branch whose content isn't integrated into the target. Commits that landed in your blind spot are by construction not integrated, so the branch is retained and you get retained_raced or retained_unmerged — you were never going to lose them. What an expected-OID adds there is "delete exactly the object I inspected", which is a different and narrower guarantee than "don't lose work".
--force-delete / -D is a different story: it skips the integration check and the CAS entirely and runs git branch -D. That is where a moved branch really can be deleted out from under you, and where a --expected-oid would carry its weight. Your phrasing — "without allowing --force to delete a changed branch" — suggests this is the case you're in, but I'd rather not guess.
So, concretely:
Does your orchestrator pass -D / --force-delete, or does it rely on the integration check?
Have you observed a branch being reused or advanced in that window, or is this hardening against a race you've reasoned about rather than hit?
Would branch_outcome plus a retry be enough for your flow, or do you need wt to fail closed against your own observed OID rather than reporting what it found?
Which of those it is decides whether a gap is left to close. The force path has no guard at all. On the default path the integration check already provides most of what an expected-OID would, and the reporting change may cover the rest — so I'd want to know it's the former before weighing new CLI surface for it. No decision either way yet.
This was written by Claude Code on behalf of max-sixty
Activity
worktrunk-bot commentedon Aug 1, 2026
Thanks for the suggestion — this fits Worktrunk's fail-closed data-safety posture well, and part of the mechanism you're asking for already exists internally.
wt removealready deletes the branch with a compare-and-swap, not a plaingit branch -d:delete_branch_if_safesnapshots the branch tip, thencas_delete_branch_outcomerunsgit update-ref -d <ref> <expected-oid>, which fails closed (RetainedRaced, branch kept) if a hook or concurrent process advanced the branch in the meantime.The gap is exactly the one you describe: that expected OID is captured by Worktrunk at its own snapshot, so the guard only covers Worktrunk's internal window (snapshot → delete) — not your orchestrator's window (preflight → the moment
wt removestarts). Between those, a branch could be reused or advanced and Worktrunk would delete against its own fresh read without knowing your expectation was already violated. And the--force/-Dpath skips the CAS entirely (branch -D, no expected value), which is the "changed branch deleted under--force" case you're calling out.So a caller-supplied token would slot in cleanly: an optional
expected_oidonRemoveArgsthreaded down to theexpected_shaargument ofcas_delete_branch_outcome, extending the existing CAS back to your observed OID and, unlike the internal snapshot, also applying on the force path. The primitive is already there; what's missing is the external interface to feed it a caller-chosen value.I'll leave it to a maintainer to weigh the exact surface (a new
--expected-oid <sha>flag vs. a branch-scoped cleanup subcommand) and prioritize.max-sixty commentedon Aug 2, 2026
Thanks — the fail-closed posture here is right, and I want to check how much of the window actually bites you before adding a flag for it.
Where things stand: #3710 makes the guard's outcome visible.
wt remove --format=jsonreported the branch asbranch_deleted: bool, which collapsed a CAS refusal into the samefalseas--no-delete-branch. It now reportsbranch_outcome, one ofdeleted,deferred,not_attempted,retained_unmerged,retained_checked_out,retained_raced,retained_failed. So a caller can already detect the race you describe and retry, where before it couldn't tell that case apart. That's an output, not the input this issue asks for — it doesn't close the request.On the input: git's primitive is there and Worktrunk already calls it (
git update-ref -d <ref> <old-oid>, which deletes only if the ref still holds<old-oid>), so the mechanism isn't the obstacle. What I'd like to understand before deciding is which window is actually costing you something.Is the concern the default path or the force path? On the default path Worktrunk won't delete a branch whose content isn't integrated into the target. Commits that landed in your blind spot are by construction not integrated, so the branch is retained and you get
retained_racedorretained_unmerged— you were never going to lose them. What an expected-OID adds there is "delete exactly the object I inspected", which is a different and narrower guarantee than "don't lose work".--force-delete/-Dis a different story: it skips the integration check and the CAS entirely and runsgit branch -D. That is where a moved branch really can be deleted out from under you, and where a--expected-oidwould carry its weight. Your phrasing — "without allowing--forceto delete a changed branch" — suggests this is the case you're in, but I'd rather not guess.So, concretely:
-D/--force-delete, or does it rely on the integration check?branch_outcomeplus a retry be enough for your flow, or do you needwtto fail closed against your own observed OID rather than reporting what it found?Which of those it is decides whether a gap is left to close. The force path has no guard at all. On the default path the integration check already provides most of what an expected-OID would, and the reporting change may cover the rest — so I'd want to know it's the former before weighing new CLI surface for it. No decision either way yet.
feat: machine-readable approval state and branch-removal outcomes (ma…