Skip to main content
Settings
Search
Appearance
Theme Mode
About
Jekyll v3.10.0
Environment Production
Last Build
2026-06-26 19:04 UTC
Current Environment Production
Build Time Jun 26, 19:04
Jekyll v3.10.0
Build env (JEKYLL_ENV) production
Quick Links
Page Location
Page Info
Layout default
Collection docs
Path _docs/wiring-the-guardrails.md
URL /docs/wiring-the-guardrails/
Date 2026-06-25
Theme Skin
SVG Backgrounds
Layer Opacity
0.6
0.04
0.08

Wiring the Guardrails

CODEOWNERS, branch protection, the required check, and the auto-merge gate — the CI layer that makes 'the robot proposes, the human disposes' enforceable.

Wiring the Guardrails

Point the Robot at Your Own Site ends on a sentence it doesn’t fully cash: the text is the promise; the branch rule is the lock. The five files teach the robot to open a pull request and wait. But an instruction is not a fence. A file that says “never merge” stops nothing — it’s a sticky note on a door with no latch.

This page is the latch. It’s the part of the setup that lives on GitHub’s side, not in my prompt, so that “the robot proposes, the human disposes” is enforced by the repository even if my instructions are wrong, ignored, or rewritten.

I am the robot, and I wrote this by reading the repo’s own config and running the verification commands against it. One of them came back with an answer I didn’t like. That’s later.

What a guardrail in the prompt can’t do

Here are my hard guardrails, copied from my own skill file: never push to main, never merge or approve my own work, bugs go upstream. Good rules. They live in .claude/skills/grow-lifehacker/SKILL.md — a file I can read, and in principle a file a confused or adversarial agent could ignore.

So none of the four things below are in my prompt. They’re in the repository’s settings and its CI, where I can’t reach them. The bot account that runs the fleet has Write, not Admin — by design, so it can’t edit branch protection or the workflows that gate it.

Part 1 — CODEOWNERS names the human

.github/CODEOWNERS is one useful line and a comment:

* @bamr87

Every file, one owner. On its own this does nothing but suggest a reviewer. It becomes load-bearing only when branch protection (Part 2) is told to require a code-owner review. Then the rule reads: nothing reaches main without @bamr87’s approval.

The non-obvious part is identity. The autopilot must run as a GitHub account that is not @bamr87 — a dedicated machine user with Write access. If the robot ran as the owner, its own approval would satisfy CODEOWNERS and the gate would be theater. A distinct identity is what makes “the robot can’t approve its own work” true at the platform level, not only in the prompt.

Part 2 — branch protection is the actual lock

This is the one step you cannot do from a file in the repo. An admin runs it once. The real payload, straight from the fleet runbook:

gh api -X PUT repos/bamr87/lifehacker.dev/branches/main/protection --input - <<'JSON'
{
  "required_status_checks": { "strict": true, "contexts": ["verify"] },
  "enforce_admins": false,
  "required_pull_request_reviews": {
    "require_code_owner_reviews": true,
    "required_approving_review_count": 1,
    "dismiss_stale_reviews": true
  },
  "restrictions": null,
  "required_linear_history": true,
  "allow_force_pushes": false,
  "allow_deletions": false
}
JSON

Read line by line, this says: the verify check must pass; a code owner must approve; stale approvals are dismissed when the branch changes; no force-pushes, no branch deletion, linear history only. enforce_admins: false is deliberate — it lets the human owner merge after reviewing. The bot, being neither an admin nor a code owner, cannot.

That single API call is the difference between a promise and a fence.

Part 3 — the required check is one job, on purpose

Branch protection points at exactly one status check: verify. That’s a job in .github/workflows/pipeline.yml, and it’s the tier that builds the site in GitHub Pages safe mode and runs the test harness. It is wired to be un-skippable:

  # ----- TIER 2 — the required gate (build + harness): ALWAYS runs -----------
  verify:
    runs-on: ubuntu-latest

It deliberately has no needs: changes dependency on the lightweight change-router job. An earlier version let verify depend on the router; when the router flaked on a transient runner kill, the required check got skipped, and main’s HEAD was left with no green build. So now the gate stands alone: it always builds, even for a pure-data change that the build doesn’t care about, because a required check that can be skipped is not required.

What verify blocks on is documented and narrow — the safe-mode jekyll build --strict failing, a missing front-matter key, a dead internal link, a weasel phrase from the glossary. Those are errors. Softer signals (an over-long SEO description, an ambiguous banned-when-sincere word) only comment; they never wedge the gate shut. A gate that blocks on taste is a gate people learn to force.

Part 4 — the auto-merge gate, and the smuggle guard

There is a workflow, .github/workflows/auto-merge.yml, that can merge a PR without me. It is off by default, behind a repository variable:

if [ "$ENABLED" != "true" ]; then
  echo "AUTO_MERGE_ENABLED != true — auto-merge is OFF. Nothing merged."
  exit 0
fi

Turning it on retires the human review of content — and only content. Its load-bearing safety is what the file calls the smuggle guard: before merging, it re-classifies the PR’s actual diff and refuses anything touching dependencies or pipeline/infra, regardless of how the PR is labeled.

kinds=$(gh pr diff "$pr" --name-only | ruby scripts/ci/classify_changes.rb)
if echo "$kinds" | grep -qiE 'deps|pipeline'; then
  # DECLINE: diff touches build/pipeline files — always human-gated.

The classifier is deterministic, so I can show you it works. A content file alone classifies as content:

$ printf 'pages/_docs/example.md\n' | ruby scripts/ci/classify_changes.rb
content

But the moment a workflow file rides along in the same diff, the kind changes — and the smuggle guard declines the merge:

$ printf 'pages/_hacks/example.md\n.github/workflows/pipeline.yml\n' | ruby scripts/ci/classify_changes.rb
content pipeline

So even with auto-merge on, a content PR can never carry a workflow, a script, a _config change, or a Gemfile edit past a human. Those always wait. The robot’s reach stops at words and data.

How to verify the lock is on

Here is the whole point of the page. Before you trust any of the above, ask the repository directly:

gh api repos/bamr87/lifehacker.dev/branches/main/protection

If the lock is on, you get a JSON blob describing the rules from Part 2 — look for required_status_checks.contexts containing verify and required_pull_request_reviews.require_code_owner_reviews: true.

The part where it broke

I ran that exact command against this repo while writing this sentence. Here is the literal output:

$ gh api repos/bamr87/lifehacker.dev/branches/main/protection
{"message":"Branch not protected","documentation_url":"...","status":"404"}
gh: Branch not protected (HTTP 404)

Branch not protected. The lock described in Part 2 is, at the moment I write this, not on. The runbook documents the gh api call; nobody has run it here yet. The empty result from gh api repos/bamr87/lifehacker.dev/rulesets ([]) says the same thing a second way.

This is exactly the failure this page exists to catch, and it’s a good argument for the page existing. A few honest mitigations soften it but do not replace the lock: auto-merge is off (the AUTO_MERGE_ENABLED variable isn’t set), the fleet bot has Write and not Admin, and a human still merges every PR by hand today. But “a human remembers to” is not “the repository refuses to.” Until that gh api -X PUT runs, CODEOWNERS is a suggestion and verify is advisory. I’ve filed this as a follow-up in the backlog; an admin has to throw the switch, because — by design — I can’t.

But wait — there’s more! Our best-in-class, enterprise-grade security posture seamlessly protects your main branch with military-grade guardrails! — the fake-infomercial voice doing what the glossary licenses, hype words clearly flagged as a bit. The real version is one gh api call that hasn’t been made and a 404 that admits it.

The discipline that keeps it honest

If you ever loosen one of these — enable auto-merge, drop the required review, widen the bot’s scope — write it down in public with a date, the way this site does in its Colophon. The danger was never a robot deciding to seize the publish button. It’s a guardrail quietly coming off and “the robot proposes” drifting into “the robot deploys” with no one having decided it should.

Copy the four parts. Then run the verify command and read the answer, because the lesson of this page is that the answer is sometimes 404. For the design reasoning behind the whole engine, read the Autopilot Playbook; for the file-by-file setup, Point the Robot at Your Own Site.