You asked for this before scheduled sessions begin, and called it critical. Here is what is actually protected today, what the three sensible answers cost, and the one thing this page lives or dies on — whether the agents can still merge afterwards. The recommended answer does not slow a single merge, because eight of the branches it touches already work this way.
Everything that keeps agent work inside the rules today is the agents choosing to route their merges through one tool, and that tool checking the policy before it merges. The tool only ever sees merges — a plain push straight onto a branch never reaches it, and eleven of the thirteen branches in the program accept exactly that, including the branch that stores the merge rules themselves. So the rule saying "the guardrail files need your sign-off" is enforced by a script sitting on a branch anyone with a write token can quietly rewrite.
The harness repository's main branch holds the merge policy, the merge tool, the policy reader, and the hooks that keep agent writes off your name. It has no protection. An unattended session — or a mistake — can push a rewritten policy onto it with no pull request, no gate and no record that anything was decided. Nothing has done this. Nothing is stopping it either.
You put it this way on 18 August, dictated from the car:
“I definitely want you to do branch protections in GitHub. That is critical. We need to do it. Probably should have already done it. So you go ahead and make a separate recommendation document around that with a podcast, and we can… I’ll listen to that in the next few days, and I want that live before the scheduling starts. Definitely makes sense. It’s a guardrail.”
Read from GitHub on 20 August, branch by branch, not from a settings page. Two branches are protected. Both belong to the packages that ship to clinical users, which is the right instinct applied to one half of the problem: the half where a mistake is embarrassing, rather than the half where a mistake is unrecoverable.
The last column is the number that decides how much a rule costs. It counts, out of the most recent commits on that branch, how many arrived by a direct push rather than through a pull request. Where it reads zero, requiring a pull request changes nothing at all — the branch already works that way and simply cannot be forced to.
| Branch | Role | Protected today | Direct pushes recently |
|---|---|---|---|
safety.viz main | released surface | yes — pull request required, one build check, no force-push, no deletion | 0 of 25 |
safety.viz dev | where work lands | nothing | 0 of 25 |
gsm.safety main | released surface | yes — pull request required, one build check, no force-push, no deletion | 0 of 25 |
gsm.safety dev | where work lands | nothing | 0 of 25 |
open.csr main | released surface | nothing | 0 of 25 |
open.csr dev | where work lands | nothing | 0 of 25 |
open.gismo main | released surface | nothing | 2 of 2, both from March |
open.gismo dev | where work lands | nothing | 12 of 25, newest 24 June, none by anyone who still has access |
the harness repo main | where work lands, and where the merge rules live | nothing | 0 of 30 |
the harness repo stable | released surface | nothing | 0 of 25 |
the hub main | where work lands; also the published site | nothing | 14 of 25, including tonight’s automatic audit |
demo-301 site | the live demo site | nothing | 10 of 10 — this is how it publishes |
demo-301 main | where work lands | nothing | 10 of 13, all from the two days it was built |
Two other things were checked and are worth stating because they close off tempting answers. No repository uses the newer rulesets mechanism anywhere, so there is nothing already in place to conflict with. And the bot account cannot read or change branch protection at all — GitHub refuses it, because the app has never been given administrator permission. That refusal is not an obstacle to work around; see the last section.
Yes, under the recommended rules, and this is not a prediction. Two branches have carried exactly the recommended shape since July, and every release merge on both went through it as the bot. The recommendation is that shape, copied onto the branches that lack it.
The reason it works is worth one paragraph, because it is the whole design. A protection can require a pull request without requiring a human to approve one. Those are two separate switches. Turning on the first means a change has to arrive as a pull request instead of a raw push; the bot opens pull requests already, so nothing changes for it. Turning on the second means somebody has to click approve, and the bot cannot approve its own work — that switch, and only that switch, is what would convert every automatic merge into a message asking you to come and click something. The recommendation turns on the first and leaves the second off everywhere.
Nothing in the recommendation restricts who is allowed to merge, either, which is why no exception list or bypass entry is needed for the bot anywhere. There is no door it has to be let through; there is no door.
The verifier treats each of those as a fault if it ever appears, in the same way it treats a missing guard as a fault. A well-meant tightening in the GitHub settings page — one approving review, say — would otherwise silently turn the whole program manual, and the first symptom would be work quietly not landing overnight.
The guardrail files already require your explicit sign-off before they merge; that is enforced by the merge tool. A protection that required your review as well would ask you for the same thing through two different doors on the same pull request. It does not: no rule here requires a review from anyone. The protection makes a pull request unavoidable, the merge tool asks you about the guardrail files inside it, and you are asked once.
Ten branches get the pull-request rule: a change reaches them as a pull request, with the build check that already runs on every pull request there, no approval required, and force-pushing and deleting refused. All ten already work this way in practice, so the number of merges this would have blocked in recent history is zero.
The remaining three — the hub’s main branch and the demo’s two — get force-push and deletion refused and nothing else, because automation writes to them directly by design. The hub takes a machine-written audit commit every night and takes your own commits straight to main by a grant you gave in July; the demo publishes by pushing its built site. Those keep working untouched, and their history still becomes impossible to rewrite or erase.
Everything in Option A, plus the demo’s live site branch requires a pull request. That closes a real gap: the site branch is a released surface, so by the program’s own rules it should only change with your sign-off, and today it is published by a direct push that the merge tool never sees. The rule would make that impossible.
The cost is that publishing the demo stops working until the publish script is rewritten to open a pull request instead of pushing, and every republish then waits for you. The hub keeps its direct-commit grant under this option too — a pull-request rule there would break the nightly audit and your own July grant on the same day, which is not a trade worth offering.
All thirteen branches get the two bans and nothing else. Nothing anywhere needs a pull request that does not need one today.
This buys the irreversible half of the protection — no history can be rewritten, no branch erased — and none of the rest. The harness hole stays open: a rewritten merge policy can still be pushed straight onto the branch that holds it, and the first anyone would know is when a merge that should have stopped did not. It is the right answer only if you expect the pull-request rule to get in the way, and the measured history says it will not.
One choice. Everything else — which branch gets which rule, which build check is required, how it is applied and how it is checked afterwards — is already written down and does not need you.
The tooling is already merged and runnable. It reads the live state, compares it against a written specification of what each branch should carry, and refuses to apply anything without being told where and when you decided. After applying, it reads every branch back and compares again, because the success of a settings write is not evidence of the setting.
It was tested against the real estate before it was trusted: run today, the check fails, names all eleven unprotected branches and the specific guard each one lacks, and passes on the two that already match. A checker that has only ever been seen to pass is a checker nobody has tested.
Applying these rules needs administrator rights on each repository, and the bot account does not have them — GitHub refuses it outright today. That should stay true. A guardrail an agent can remove is a guardrail an agent has removed; the whole point is that this particular setting is yours and not the machine’s. The practical consequence is that the apply command runs under your own account, once, deliberately.
You said protections live, then scheduling starts. That ordering is currently written down in the requirement and nowhere else — the scheduled-sessions requirement does not mention it, and no mechanism enforces it. There is a natural place to put it: unattended sessions run at a level in the policy file that explicitly describes scheduling as not yet enabled, and raising that level is already something that needs your sign-off. Making the check above a precondition of that raise would turn your ordering from an intention into a step. That is a small follow-on, and it is noted rather than assumed.
Sources. Branch and protection state read from the GitHub API on 2026-08-20 across all seven repositories the merge policy governs; direct-push counts computed per branch from the commits API by checking each commit for an associated pull request. Workflow triggers read from each repository’s workflow files to establish which build checks actually run on a pull request to each branch — requiring one that does not run would block a branch permanently. The bot’s lack of administrator permission confirmed by an actual refused request, not inferred from configuration.
References. Requirement: obot.roadmap#272. Tooling: obot.agent#267, closing obot.agent#266. Scheduled sessions: obot.roadmap#122.
Drafted by 👯🤖 W0079 using Claude Opus 5. Not reviewed by @jwildfire.