The babysit-prs helpers that take --apply perform their mutations inside their own process. A host permission classifier decides on the outer shell command only, so nothing downstream can reconsider the individual operation:
refresh_pr_branch.py --apply issues PUT repos/{repo}/pulls/{n}/update-branch. No host rule governing gh sees that call.
prune_babysit_worktrees.py --apply runs git worktree remove. No host rule governing git sees that either.
manage_babysit_lease.py and manage_feedback_ledger.py write state files.
request_review.py --apply posts a comment — the one exception, because the comment body arrives in argv and is therefore visible to whatever reads the command line.
The consequence is that a host grant covering these scripts is necessarily a grant of whatever the script does, not of the operation the operator had in mind. Two of this skill's own components already solve exactly this and are the model:
babysit_merge.py is check-only unless --merge is passed, so the mutating mode is selected by a flag readable from the command text.
source-control-babysit-resolve-thread enforces bot-only participation and the outdated check in code, and refuses a bulk resolve with no thread pin.
Both make the permitted operation a property the invocation can be checked against. The --apply helpers do not.
Ask
Give each mutating mode a wrapper (or an in-script guard) that mechanically validates the single operation it is permitted, matching the merge gate's and the resolver's shape — so a host grant can name a wrapper whose behavior is bounded rather than a script whose --apply branch is unbounded.
Worth noting that two of the four are already structurally safe by accident of implementation, and locking that in is part of the ask rather than new work:
refresh_pr_branch.py pins expected_head_sha and validate_current_candidate refuses when headRefOid != expected_head_sha. Server-side and head-pinned, so it cannot force-push or rewrite history.
prune_babysit_worktrees.py calls git worktree remove without --force and classifies a dirty worktree as keep_dirty, so it cannot discard uncommitted work.
Those are properties of the current implementation with no test asserting them, which is the gap: a future refactor could add --force and nothing would notice.
Context
Found by the Codex reviewer on melodic-software/dotfiles#315, which hardens the host-side autoMode prose that grants these scripts. That PR removed the sentence claiming the nested actions stayed governed by its gh and git entries, because the claim is not checkable. It now records the structural properties above as properties of today's installed code rather than as guarantees, and points at this issue as the real fix.
Related: #843 (plugin bin/ not on the Bash tool PATH, which is why host grants still name raw script paths).
The babysit-prs helpers that take
--applyperform their mutations inside their own process. A host permission classifier decides on the outer shell command only, so nothing downstream can reconsider the individual operation:refresh_pr_branch.py --applyissuesPUT repos/{repo}/pulls/{n}/update-branch. No host rule governingghsees that call.prune_babysit_worktrees.py --applyrunsgit worktree remove. No host rule governinggitsees that either.manage_babysit_lease.pyandmanage_feedback_ledger.pywrite state files.request_review.py --applyposts a comment — the one exception, because the comment body arrives in argv and is therefore visible to whatever reads the command line.The consequence is that a host grant covering these scripts is necessarily a grant of whatever the script does, not of the operation the operator had in mind. Two of this skill's own components already solve exactly this and are the model:
babysit_merge.pyis check-only unless--mergeis passed, so the mutating mode is selected by a flag readable from the command text.source-control-babysit-resolve-threadenforces bot-only participation and the outdated check in code, and refuses a bulk resolve with no thread pin.Both make the permitted operation a property the invocation can be checked against. The
--applyhelpers do not.Ask
Give each mutating mode a wrapper (or an in-script guard) that mechanically validates the single operation it is permitted, matching the merge gate's and the resolver's shape — so a host grant can name a wrapper whose behavior is bounded rather than a script whose
--applybranch is unbounded.Worth noting that two of the four are already structurally safe by accident of implementation, and locking that in is part of the ask rather than new work:
refresh_pr_branch.pypinsexpected_head_shaandvalidate_current_candidaterefuses whenheadRefOid != expected_head_sha. Server-side and head-pinned, so it cannot force-push or rewrite history.prune_babysit_worktrees.pycallsgit worktree removewithout--forceand classifies a dirty worktree askeep_dirty, so it cannot discard uncommitted work.Those are properties of the current implementation with no test asserting them, which is the gap: a future refactor could add
--forceand nothing would notice.Context
Found by the Codex reviewer on melodic-software/dotfiles#315, which hardens the host-side
autoModeprose that grants these scripts. That PR removed the sentence claiming the nested actions stayed governed by itsghandgitentries, because the claim is not checkable. It now records the structural properties above as properties of today's installed code rather than as guarantees, and points at this issue as the real fix.Related: #843 (plugin
bin/not on the Bash tool PATH, which is why host grants still name raw script paths).