drive-pr "The Drive Loop" step 4 and merge-and-release "The Procedure" step 8 both end a task's cleanup with git push origin --delete -- "<branch>", and both place that delete after the worktree has been removed. Once the worktree is gone, the base clone is the only checkout left to run it in, and the base clone is a primary checkout.
gh-write-guard.py denies push in a primary checkout unconditionally, by design. Its own comment beside the denied-subcommand set states the reason: "A push doesn't mutate the local working tree or HEAD the way the rest of this set does, but it publishes whatever is there, and no documented fleet workflow ever pushes from a primary checkout: every push runs from a task's own worktree instead." That premise is what the two procedures above contradict, since they are documented fleet workflows and the step they prescribe has nowhere else to run.
Observed, driving a feature pull request to develop on a machine carrying the agent-safety install, following step 4 in the order it is written:
$ git -C <base-clone> push origin --delete -- "<branch>"
This `git push` runs directly against a primary checkout (<base-clone>), not a linked
worktree. A mutating git operation there can destroy another task's uncommitted work.
The delete still has to happen, so the agent either reaches for GH_WRITE_GUARD_ALLOW_PRIMARY_CHECKOUT, which is a grant it must not give itself and which would lift the guard for every other mutating subcommand too, or it substitutes something the procedure does not name. What it actually substituted was gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/<branch>, which is a GitHub write rather than a git one and so is judged by the gh-write rules instead, within the same owner and against an object id read live in the same run.
Two other facts bound the fix:
git branch -D in the base clone is not denied, so only the remote half of the cleanup is blocked. Measured in the same run: git -C <base-clone> branch -D <branch> succeeded.
- The worktree cannot simply be kept until after the delete in
merge-and-release step 8, whose first part removes the promotion worktree specifically so the base clone can check develop out.
Candidate fixes, one to pick rather than a menu to carry:
- Reorder both procedures so the verify-then-delete of the remote branch runs from the task's own worktree, before that worktree is removed. This keeps the guard's premise true and needs no new mechanism. The verification the two skills already require,
headRefOid captured before the merge against git ls-remote --heads --exit-code, is unaffected by where it runs.
- Name
gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/<branch> as the prescribed form for the remote delete, on the grounds that deleting a merged remote branch is a GitHub write and never needed a local checkout at all.
- Exempt
push --delete in the guard. This is the weakest of the three: the flag shape is easy to spoof next to a --delete that is not what it looks like, and it widens the one subcommand whose denial is about publishing rather than about local destruction.
Done is one of those chosen and applied to every surface stating this step, which is drive-pr step 4, merge-and-release step 8, and repo-worktree "Listing and Cleanup" where it describes the same remote delete, with the guard's own comment updated if and only if fix 3 is the one chosen.
drive-pr"The Drive Loop" step 4 andmerge-and-release"The Procedure" step 8 both end a task's cleanup withgit push origin --delete -- "<branch>", and both place that delete after the worktree has been removed. Once the worktree is gone, the base clone is the only checkout left to run it in, and the base clone is a primary checkout.gh-write-guard.pydeniespushin a primary checkout unconditionally, by design. Its own comment beside the denied-subcommand set states the reason: "A push doesn't mutate the local working tree or HEAD the way the rest of this set does, but it publishes whatever is there, and no documented fleet workflow ever pushes from a primary checkout: every push runs from a task's own worktree instead." That premise is what the two procedures above contradict, since they are documented fleet workflows and the step they prescribe has nowhere else to run.Observed, driving a feature pull request to
developon a machine carrying the agent-safety install, following step 4 in the order it is written:The delete still has to happen, so the agent either reaches for
GH_WRITE_GUARD_ALLOW_PRIMARY_CHECKOUT, which is a grant it must not give itself and which would lift the guard for every other mutating subcommand too, or it substitutes something the procedure does not name. What it actually substituted wasgh api -X DELETE repos/<owner>/<repo>/git/refs/heads/<branch>, which is a GitHub write rather than a git one and so is judged by the gh-write rules instead, within the same owner and against an object id read live in the same run.Two other facts bound the fix:
git branch -Din the base clone is not denied, so only the remote half of the cleanup is blocked. Measured in the same run:git -C <base-clone> branch -D <branch>succeeded.merge-and-releasestep 8, whose first part removes the promotion worktree specifically so the base clone can checkdevelopout.Candidate fixes, one to pick rather than a menu to carry:
headRefOidcaptured before the merge againstgit ls-remote --heads --exit-code, is unaffected by where it runs.gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/<branch>as the prescribed form for the remote delete, on the grounds that deleting a merged remote branch is a GitHub write and never needed a local checkout at all.push --deletein the guard. This is the weakest of the three: the flag shape is easy to spoof next to a--deletethat is not what it looks like, and it widens the one subcommand whose denial is about publishing rather than about local destruction.Done is one of those chosen and applied to every surface stating this step, which is
drive-prstep 4,merge-and-releasestep 8, andrepo-worktree"Listing and Cleanup" where it describes the same remote delete, with the guard's own comment updated if and only if fix 3 is the one chosen.