Branch Protection Setup¶
This doc describes the intended protection rules for main. Use the verification
command below to inspect the live configuration before proposing or applying
changes; this example is not evidence that every setting is enabled.
Requires repo admin access.
gh auth statusmust show admin scope onjckeen/dotfiles.
What we want on main¶
- Require a pull request before merging
- Require 1 approving review
- Require status checks to pass:
shellcheck,tsc,python-lint,doc-truth,checks,agentpack-generated, and the twosmoke-installmatrix jobs (setup.sh syntax + --help (ubuntu-latest)andsetup.sh syntax + --help (macos-latest)) - Require branches to be up to date before merging
- Require signed commits
- Block force pushes
- Block direct pushes (everything goes through a PR)
- Include administrators (no bypass)
Apply via gh api¶
Save this as the JSON body. The structure is the GitHub Branch Protection
API contract — every nullable field must be present, even if null.
cat > /tmp/main-protection.json <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": [
"shellcheck",
"tsc",
"python-lint",
"doc-truth",
"checks",
"agentpack-generated",
"setup.sh syntax + --help (ubuntu-latest)",
"setup.sh syntax + --help (macos-latest)"
]
},
"enforce_admins": true,
"required_pull_request_reviews": {
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"required_approving_review_count": 1,
"require_last_push_approval": false
},
"required_signatures": true,
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false,
"block_creations": false,
"required_conversation_resolution": true,
"lock_branch": false,
"allow_fork_syncing": true
}
JSON
gh api \
-X PUT \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
repos/jckeen/dotfiles/branches/main/protection \
--input /tmp/main-protection.json
Signed-commit enforcement is a separate sub-resource on some accounts:
gh api \
-X POST \
-H "Accept: application/vnd.github+json" \
repos/jckeen/dotfiles/branches/main/protection/required_signatures
Verify¶
gh api repos/jckeen/dotfiles/branches/main/protection | jq '{
reviews: .required_pull_request_reviews.required_approving_review_count,
checks: .required_status_checks.contexts,
signed: .required_signatures.enabled,
force_push: .allow_force_pushes.enabled,
admins: .enforce_admins.enabled
}'
Target output for this configuration:
{
"reviews": 1,
"checks": ["shellcheck", "tsc", "python-lint", "doc-truth", "checks", "agentpack-generated", "setup.sh syntax + --help (ubuntu-latest)", "setup.sh syntax + --help (macos-latest)"],
"signed": true,
"force_push": false,
"admins": true
}
Compare contexts as a set; GitHub may return them in a different order. Inspect any differences before changing the live rule.
Notes¶
- The status-check contexts must match the job names:
shellcheck,tsc,python-lint,doc-truth,checks, andagentpack-generatedlive in.github/workflows/ci.yml; the twosetup.sh syntax + --help (...)contexts are the matrix jobs of.github/workflows/smoke-install.yml. If you rename a job, update both this doc and the protection rule. smoke-installis gating as of 2026-07 (#203): itscontinue-on-errorwas removed, and it always runs (nopaths:filter — a first step skips the real work when no smoke-relevant file changed) so it can be protection-required without non-matching PRs hanging on "Expected".agentpack-generated(gen-agentpack.sh --check) has nopaths:filter, so it reports on every PR and is safe to require (#237) — a manifest-stale PR that editsclaude/skills/*/SKILL.mdorclaude/agents/*.mdwithout regeneratingclaude/AGENTPACK.yamlis blocked at merge, not noticed after.python-lint(ruff check + format --check over every*.py, plus the gate schema's metaschema check) has nopaths:filter, so it reports on every PR and is safe to require. Add it to the live rule only after the PR that introduces the job has merged: a required context that no open PR can produce leaves them hanging on "Expected".checksis required: it speaks for the behavioral suites, gate self-tests, parity checks, and install-integrity checks. It reports on every PR, so a failing test must block merging even when the standalone lint jobs pass. Since #472 those suites run in thechecks-shardsmatrix job (checks (receipts),checks (gates),checks (runtime),checks (checkers)), andchecksis a tiny aggregator thatneedsthem and fails unless every shard succeeded. It runs underalways(), because branch protection reads a skipped required job as success: a run cancelled after a shard went red must leave this context red, not absent (#520). A matrix job produces one status context per leg rather than one for the set, so the aggregator is what keeps a single required name. Do not require the individual shard contexts: their names change whenever the grouping is rebalanced.secret-scanandcommit-formatrun on every PR. This example does not require their contexts; consult the live protection rule before calling a check advisory.required_signatures: truerejects unsigned commits at the server. Configuregit config --global commit.gpgsign trueandgpg.format ssh(or GPG) on every machine that pushes tomain.- To temporarily relax for a one-off rescue:
gh api -X DELETE repos/jckeen/dotfiles/branches/main/protection, do the work, then re-apply this file. Don't leave it off.
The codex-review ruleset (layered on top)¶
main also carries a separate repository ruleset named codex-review, created
by claude/scripts/fleet-codex-review.sh (which applies the same ruleset across
the fleet). It requires the codex-review commit status, posted by
.github/workflows/codex-review.yml while the GitHub Codex bot reviews a PR,
and it requires every review thread to be resolved before merging, so the bot's
findings are fixed or explicitly declined before auto-merge fires. It is a
ruleset rather than part of the protection body above so it can be added or
removed without rewriting this rule:
gh api repos/jckeen/dotfiles/rulesets --jq '.[] | select(.name == "codex-review")'
To lift it for one PR, add the codex-skip label. To remove it, delete the
ruleset by id (gh api -X DELETE repos/jckeen/dotfiles/rulesets/<id>).
Finding which PRs wait on bot findings needs no polling:
.github/workflows/codex-findings.yml labels a PR codex-findings while it
has an unresolved Codex bot thread, so gh pr list --label codex-findings is
the queue. Answer each thread with claude/scripts/codex-threads.sh: list
--pr N, then resolve --thread ID --reply "Fixed in <sha>" or a reason it
is moot. Resolving a PR's last bot thread this way also clears its label;
resolving one in the web UI clears it at the bot's next review.