The doorman.

Branch protection is a server-side rule that says "you cannot do X to main." CODEOWNERS is a file that says "if these paths change, these humans must approve." Together they're how teams keep main safe from accidents.

Concept 01 — A rule that lives on the server

The apartment doorman.

Imagine an apartment building. Anyone can walk up to the front door. But there's a doorman, and his job is to check three things before letting you in: are you on the list, have you been signed in by another resident, and are you carrying nothing that violates building policy. If any of those fail, the doorman politely sends you away.

Branch protection is exactly the same idea, applied to main. It's a set of rules that GitHub's server checks every time anyone tries to push or merge to a protected branch. If the push matches all the rules, it lands. If not, GitHub rejects it with an error message — and the official version of the project is untouched.

What "no protection" looks like

Without any rules in place, anyone with push access (today: just you) can run git push --force and overwrite the entire history of main. That's not a typo — force-push literally rewrites which commits exist, throwing away anything you didn't keep. A single bad command on a tired Friday afternoon can destroy weeks of work. Branch protection is what stops that.

Concept 02 — What you can gate

Four common rules.

You can mix and match. Below are the four rules AL-2.1 tried to set on your repo — these are the ones almost every team uses:

require_code_owner_reviews
"The people listed in CODEOWNERS must approve."
If a pull request touches frontend/ and CODEOWNERS says Josh owns frontend, Josh must click Approve before merge is allowed. We'll cover CODEOWNERS in the next concept.
required_approving_review_count: N
"At least N humans must approve."
Common values are 1 (small team) or 2 (sensitive code). Today this is set to 0 in your config — because you're solo and can't approve your own pull requests. It flips to 1 the moment Josh joins.
required_linear_history: true
"No merge commits — keep history clean."
Forces merges to be "squashed" or "rebased" rather than producing a "Merge branch X into main" commit. Easier to read history; easier to revert later.
allow_force_pushes: false
"Nobody can rewrite history."
This is the big one. Force-pushing to main is the single most destructive thing you can do. With this set to false, GitHub refuses every force-push to main, no matter who you are.
What AL-2.1 sent to GitHub

Here's the literal request the AL-2.1 worker tried to make — and was rejected for (we'll get to why in Lesson 3):

gh api -X PUT "repos/lovebuilt/agentic-lab/branches/main/protection" \ -F required_pull_request_reviews.require_code_owner_reviews=true \ -F required_pull_request_reviews.required_approving_review_count=0 \ -F required_linear_history=true \ -F allow_force_pushes=false \ -F allow_deletions=false # GitHub's response: HTTP 403 — "Upgrade to GitHub Pro or make this repository public to enable this feature."
Concept 03 — CODEOWNERS, the auto-router

A text file that says who owns what.

CODEOWNERS is just a text file. You commit it like any other file. GitHub looks for it at a known location (the root of the repo, or inside .github/) and reads it. Each line maps a file pattern to a GitHub username:

# CODEOWNERS — at the root of your AL repo # Format: <path pattern> <@usernames> * @lovebuilt # default: you own everything skills-lab/ @lovebuilt # skills lab — you hooks-lab/ @lovebuilt # hooks lab — you .private/ @lovebuilt # your private files

When Josh's GitHub username is added to the repo, CODEOWNERS expands to @lovebuilt @josh-gh on shared rows, plus per-lab rows where one of you takes lead. Touching frontend/? Josh's review gets auto-requested. Touching backend/? Yours does.

Here's the part that surprises everyone: the CODEOWNERS file works by itself, even without branch protection — but only as documentation. GitHub still reads it and shows ownership in the web UI. What it can't do, without protection, is block a merge. That's the doorman's job.

Concept 04 — With vs without protection

The sign vs the barrier.

This is the single most important concept in this lesson. Same CODEOWNERS file. Same git history. Two completely different outcomes depending on whether branch protection is turned on.

Without protection

CODEOWNERS is a sign

"Please don't park here."

The file is committed and visible in the UI. GitHub even auto-requests reviews from listed owners. But anyone with push access can still merge straight to main with no review and no blocker.

Useful as documentation. Useless as enforcement.

With protection

CODEOWNERS is a barrier

A bollard. You can't drive past.

The same file becomes a hard gate. GitHub refuses every merge to main until the code owners on the touched paths click Approve. Force-pushes bounce. Direct pushes bounce. The doorman is on duty.

Documentation and enforcement.

Why this matters for you specifically

The AL repo already has a CODEOWNERS file committed (AL-2.1 landed it on 2026-05-18 with @lovebuilt as the sole owner). So the "sign" side is live today — GitHub knows you own everything. The "barrier" side is what's missing. Flipping it on is what the next lesson is about.

Concept 05 — Your repo, right now

What's actually live.

★ The state on 2026-05-19

One door, no doorman.

CODEOWNERS · committed at repo root, content: * @lovebuilt (plus 3 per-lab rows). Visible in GitHub UI. Documentation works.

Branch protection · not configured. AL-2.1 attempted it; GitHub returned HTTP 403 with the paywall message. The repo is unprotected.

Force-push to main · currently allowed. Nothing stops you (or anyone with push access) from rewriting history.

Direct push to main · currently allowed. No pull request required.

This is fine for solo work — you're not going to force-push your own repo accidentally. It stops being fine the moment Josh joins, which is exactly the timeline that Lesson 3 is about.

✦ Lesson 2 recap

What you now know about branch protection…