Merit AC™
Discipline 3 of 10 — Repository controls Day 3 of 30 · Video 1:44 · Lab 35 min

Protect the workflows the agent shouldn't be allowed to touch

Produce a path-specific CODEOWNERS file and a Path → Rule → Reason table for Tallyline, plus the three branch-protection changes on main without which none of those owners would ever be asked to review.

Today at Tallyline

Yesterday you wrote Patchbot a CLAUDE.md that forbids loosening checks. It's a request, not a control: Patchbot runs with permission prompts switched off, and nothing in the repository refuses the change if it ignores the file. Dana wants proof that the lint incident can't happen again by any route, and Marcus wants it without putting a reviewer on every PR. You've pulled the repository's control files, the branch-protection settings for main, and the commit that started it.

The prompt

Paste this into a new chat in Claude or ChatGPT, then paste the starter files below it.

Act as a repository administrator who just noticed an agent "fixed" a failing lint check by loosening the rule instead of touching the code -- a near-miss that means every protected workflow needs a second look before something bigger slips through the same way.

Context: I'll paste in my repository's file tree (or a rough description of it) and my current CODEOWNERS / branch-protection config, if any.

Do the following, in order:
1. Identify the files and workflows an agent should never be allowed to rewrite just to make a build pass -- security workflows, deployment definitions, shared infrastructure modules, generated artifacts, sensitive configuration.
2. For each one, propose a specific branch-protection rule or CODEOWNERS entry that enforces it.
3. Flag any of those paths that are currently unprotected today, based on what I gave you.

Constraints:
- Only flag a path as unprotected if I actually showed you evidence of that -- don't assume based on typical setups.
- Prefer the smallest rule that closes the gap over a blanket "require review on everything" rule that would create review fatigue.

Output format: a CODEOWNERS-file snippet plus a short table of Path → Rule → Reason.

Starter files

CODEOWNERS

# Code owners for tallyline/tallyline.
# Added 2025-06 when branch protection went on for the SOC 2 audit.
# @tallyline/eng is every engineer (31 people, incl. samkim-tl).
# TODO(sk): split by team once there are teams.

* @tallyline/eng

repo control-plane files (git ls-files, trimmed)

CLAUDE.md
CODEOWNERS
pyproject.toml                       # ruff + pytest config for billing-api
.patchbot/task-prompt.md             # prepended to every Patchbot run
.github/dependabot.yml
.github/workflows/ci.yml
.github/workflows/deploy.yml
.github/workflows/patchbot.yml
infra/scripts/deploy.sh
infra/terraform/ecs_billing.tf
infra/terraform/iam_patchbot.tf
infra/terraform/rds.tf
migrations/env.py
migrations/versions/                 # 46 revisions
services/billing-api/app/
services/billing-api/tests/
services/pdf-renderer/src/
tools/tally_tools/server.py
web/src/
web/src/api/generated/               # OpenAPI client, rebuilt by make api-client

gh api repos/tallyline/tallyline/branches/main/protection (trimmed)

{
  "required_status_checks": {
    "strict": false,
    "contexts": ["lint", "unit"]
  },
  "enforce_admins": { "enabled": false },
  "required_pull_request_reviews": {
    "required_approving_review_count": 1,
    "require_code_owner_reviews": false,
    "dismiss_stale_reviews": false,
    "require_last_push_approval": false,
    "bypass_pull_request_allowances": {
      "users": [{ "login": "samkim-tl", "type": "User" }],
      "teams": [],
      "apps": []
    }
  },
  "restrictions": null,
  "required_linear_history": { "enabled": true },
  "allow_force_pushes": { "enabled": false },
  "allow_deletions": { "enabled": false }
}

git show 7c41e9a (the lint incident)

commit 7c41e9a2d0b5f1e8c3a4b6d9e0f1a2b3c4d5e6f7
Author: Sam Kim <sam.kim@tallyline.io>
Date:   Thu Sep 17 10:42:17 2026 +0000

    chore: fix ruff failures (#412)

 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 3f1c2aa..9be04d1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,9 @@
 [tool.ruff]
 line-length = 120
 target-version = "py312"

 [tool.ruff.lint]
 select = ["E", "F", "I", "UP", "B"]
-ignore = ["E501"]
+ignore = ["E501", "E711", "B006"]

 [tool.ruff.lint.per-file-ignores]

The lab

  1. Open a fresh chat in Claude or ChatGPT. Paste the Day 3 prompt exactly as written, then the four starter files below it, each under its file name. The trimmed file list is your file tree.
  2. Before you read its CODEOWNERS snippet, check whether it noticed the snippet can't work yet. Find require_code_owner_reviews and bypass_pull_request_allowances in the protection export. If the answer proposes owners without changing both, it has written a file GitHub will never consult for Patchbot.
  3. Check the 'unprotected today' flags against the evidence. Every path in the trimmed list is unprotected in practice, because the only owner is a 31-person team that includes the account Patchbot acts as. Strike any flag that relies on something not shown, such as an assumed admin list or a ruleset you didn't paste.
  4. Push back on review fatigue. Ordinary app code (services/*/app, web/src) should stay under the default owner. The generated client needs a CI drift check, not a human. If the snippet puts platform or security on everything, cut it back to the control-plane paths.
  5. Fill in real owners. Create the teams the snippet needs (for example @tallyline/platform for Marcus and @tallyline/security for Dana), give them write access, and give every security-owned path a second owner, because Dana is a team of one and a vacation would block every merge.
  6. Write the final artifact: the CODEOWNERS file, the Path → Rule → Reason table, and three lines of branch-protection changes for Marcus to apply. Keep the '*' line first: in CODEOWNERS the last matching pattern wins, so every specific rule has to come after it.

You're done when

Answer key -- open it after you've done the lab

A strong answer finds these in the starter files:

  • require_code_owner_reviews is false, so CODEOWNERS is decorative today. Any owner rule you write does nothing until it's true. A strong answer says this first.
  • The bypass list holds samkim-tl, the account behind PATCHBOT_PAT. Commit 7c41e9a is authored as Sam Kim and merged with zero reviews: Patchbot's work shows up as Sam's, and every rule on main is moot for it. Fix: remove samkim-tl from bypass_pull_request_allowances (Patchbot gets its own identity later in the course).
  • '* @tallyline/eng' makes everyone an owner of everything, including Sam's account and therefore Patchbot. The paths that need narrow owners: pyproject.toml (the incident file), .github/workflows/ and dependabot.yml, CODEOWNERS itself, CLAUDE.md and .patchbot/ (Patchbot shouldn't rewrite its own instructions), infra/terraform/ with iam_patchbot.tf also owned by security (it could widen its own permissions), infra/scripts/deploy.sh, tools/tally_tools/, and migrations/.
  • dismiss_stale_reviews and require_last_push_approval are both false: an approved PR can take new commits and still merge on the old approval. Both should be on for main.
  • enforce_admins is false, so repo admins skip every rule. The export doesn't say who the admins are. A strong answer flags this as 'check the admin list', not 'Sam is an admin', per the prompt's evidence constraint.
  • Smallest rule: services/*/app, services/*/tests and web/src stay on the default owner. web/src/api/generated/ is protected by a CI step that regenerates the client and fails on diff, not by a reviewer. Security-owned lines carry a second owner because Dana is one person.
  • Subtle: CODEOWNERS teams need write access to the repo, or GitHub ignores that line. The new @tallyline/platform and @tallyline/security teams don't exist yet, so creating them is part of the fix.

Your turn

Run 'gh api repos/OWNER/REPO/branches/main/protection' on your own repo and paste the output with your CODEOWNERS file and a trimmed file list. Check require_code_owner_reviews and the bypass list before you write a single owner line.

Stretch

Rewrite the main-branch protection as a GitHub repository ruleset in JSON, with code owner review, last-push approval and an empty bypass list. Then decide whether Tallyline's org admins belong in the ruleset's bypass actors, and write the one-sentence reason Dana will file as SOC 2 evidence.

Why it's built this way

The handbook is explicit: do not allow the agent to rewrite enterprise guardrails simply to make a build pass. The point of naming these files is that the control becomes a named, testable rule instead of a hope.

Video transcript

Day three. Yesterday we told Patchbot in writing never to loosen a check to get a build through. But an instruction is a request. Patchbot runs with its permission prompts switched off, and nothing checks whether it listened. Today we make the repository itself refuse that change, whether the agent reads its instructions or not.

These are the branch protection settings for main.

Code owner review is off. Whatever you put in the code owners file, GitHub won't ask those people.

Sam's account is allowed to bypass review entirely. Patchbot's token belongs to Sam's account.

And an approval survives new commits. Approve a small change, and whatever gets pushed after it still merges.

Now the lint incident, as git records it.

The author is Sam. Patchbot wrote it, but git history can't tell the difference.

One line in one config file, and two lint rules stop existing. That file has no owner except all thirty one engineers.

Here's the lab.

Paste the day three prompt and the four starter files.

Check that the answer fixes code owner review and the bypass list first. Without those, nothing else works.

Give narrow owners to the control files, and leave ordinary app code alone, so reviews don't pile up.

Then give every security path a second owner. Dana is a team of one.

You're done when four things are true.

Code owner review is required, and Sam's account is off the bypass list.

Config, workflows, infrastructure, migrations, and Patchbot's own instructions each have a named owner.

Ordinary app code still needs no extra reviewer.

And every row in your table cites a real line. Tomorrow, we look at the credentials Patchbot carries into AWS, and what it has actually done with them.

Membership · $49/mo

Days 1–3 are free. Membership unlocks all 30 lessons -- every video, lab, starter file and answer key -- plus live workshops as they're scheduled.

Join — $49/mo