Decision artifact2026-08-20awaiting your decisionone question

Branch protections: what gets locked down before the clock starts

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.

Branches the merge rules govern: 13 Protected today: 2 Merges the recommendation would block: 0 Your remaining act: pick A, B or C
THE SITUATION

Three sentences

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 specific hole

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.”
MEASURED

What is protected right now

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.

BranchRoleProtected todayDirect pushes recently
safety.viz mainreleased surfaceyes — pull request required, one build check, no force-push, no deletion0 of 25
safety.viz devwhere work landsnothing0 of 25
gsm.safety mainreleased surfaceyes — pull request required, one build check, no force-push, no deletion0 of 25
gsm.safety devwhere work landsnothing0 of 25
open.csr mainreleased surfacenothing0 of 25
open.csr devwhere work landsnothing0 of 25
open.gismo mainreleased surfacenothing2 of 2, both from March
open.gismo devwhere work landsnothing12 of 25, newest 24 June, none by anyone who still has access
the harness repo mainwhere work lands, and where the merge rules livenothing0 of 30
the harness repo stablereleased surfacenothing0 of 25
the hub mainwhere work lands; also the published sitenothing14 of 25, including tonight’s automatic audit
demo-301 sitethe live demo sitenothing10 of 10 — this is how it publishes
demo-301 mainwhere work landsnothing10 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.

THE QUESTION THIS LIVES OR DIES ON

Can the agents still merge?

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 switches that would break it, all left off

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.

And your sign-off is not asked for twice

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.

OPTIONS

Three answers, in order of how much they change

Option A — recommended

Lock what can be locked for free, guard the rest

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.

  • Closes the harness hole completely: the merge rules can no longer be changed without a pull request.
  • Costs no merge, no lane and no habit. Nothing you do today stops working.
  • Leaves one gap open on purpose, described under Option B.
Option B — stricter

Option A, and the demo site publishes through a pull request too

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.

  • Closes a genuine hole in the release rules for the demo.
  • Breaks demo publishing until a script is changed, and adds you to the loop each time.
  • Reasonable to choose later, once the demo is something people look at.
Option C — minimal

Only refuse force-pushes and deletions, everywhere

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.

  • Absolutely zero behaviour change anywhere.
  • Leaves the reason this requirement exists unaddressed.
DECISION
P1

Which set of protections gets applied?

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.

Recommendation: Option A. It closes the hole the requirement was written about, it costs nothing that anyone does today, and it is the shape two branches have already been running under the bot for a month. Option B is the same thing plus one workflow change, and is easy to add later. Option C leaves the reason for the exercise untouched.

What unblocks on each

AFTERWARDS

How it gets applied, and how you know it worked

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.

obot.agent/scripts/obot-protect read # what GitHub enforces right now obot.agent/scripts/obot-protect plan # what would change; writes nothing obot.agent/scripts/obot-protect verify # fails if live state and spec disagree obot.agent/scripts/obot-protect apply --approved '<where and when you chose>'

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.

One thing to keep as it is

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.

On the ordering you asked for

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.