Owner: the founder. Approved: 2026-10-08. Review: annually.
local main == origin/main == production. Always. Every rule below exists to
protect that sentence.
It is written this bluntly because it was broken once. On 2026-05-21 the local
main and origin/main had diverged into two different codebases, because
commits had landed directly on a local main that was never pulled and deploys
had been done by copying individual files to the server. The result was a
production system that matched no single git ref — nothing could be reasoned
about, reproduced or rolled back.
git checkout main && git pull origin main before
branching. A stale main is how divergence begins.feature/*, fix/* or chore/*. Never commit to main — not
locally, not in a worktree.feat:, fix:, chore:, docs:,
refactor:, test:). The commit hook (scripts/git-hooks/pre-commit) runs
.pre-commit-config.yaml on the staged files: ruff, gitleaks,
detect-private-key and the file-hygiene checks. mypy is left to the
pre-push gate — minutes on a fresh worktree, too slow for every commit.scripts/check.py, run by the git pre-push
hook, refuses the push unless every stage passes: ruff (including the S
security ruleset), mypy --strict, gitleaks over the branch's commits and
over the whole history, trivy fs over the lockfile and Dockerfile (CRITICAL
blocks), the pytest suite, the Redis-backed counter checks, the Schemathesis
API fuzz, and the live Postgres stage that upgrades to head, downgrades,
re-upgrades and then asserts that RLS actually bites as the runtime role
(ADR-0119). Both hooks are installed once per clone with
scripts/install_git_hooks.sh.origin/main.main immediately, so it cannot drift.origin/main with
uv run python infrastructure/deploy.py --apply. The script builds a
deterministic tarball from tracked files only, rsyncs with --delete so
removed files actually disappear, refuses unless the server .env exists
with mode 0600 or tighter (it never writes one), brings the compose stack
up, and polls /health until it answers 200.Per CLAUDE.md, the founder decides — not the implementer — on: a new external
service or vendor, any change to the architectural principles, a major-version
dependency upgrade, a schema change requiring migration, anything costing more
than $20/month, anything affecting the security posture, pricing, and any major
customer-facing copy or UX change. Everything else (library choice inside the
stack, code structure, tests, refactors, bug fixes with a clear root cause,
logging, semver-compatible dependency updates) is decided autonomously.
Architectural decisions are recorded as ADRs in docs/adrs/ before the change
lands, not after it ships.
Copying a file to the server with scp is forbidden as a routine deploy. It
is the tool that produced the divergence in §1. As an emergency hotfix it is
permitted only under these conditions, all of them:
origin/main afterwards;deploy.py --apply run follows, to re-converge production onto the ref;Redeploy the previous origin/main ref. Because the deploy is a whole-tree
rsync of a git archive, "the previous version" is a commit, not a reconstruction.
Migrations are the exception that needs thought: a rollback across a migration
requires the corresponding downgrade, which the pre-push gate has already
exercised for the recent chain.
Every merged PR is a dated change record with its description — reviewed by
the gate and by its author, not by a second person; the git history is the
change log; deploy.py output is the deployment record. For the auditor, the
export is a list of merged PRs for the period plus the output of a full gate
run — see docs/security/evidence-checklist.md §3.
Branch protection on origin/main is discipline, not a GitHub setting — and
today it cannot be one: GitHub branch protection and rulesets are not available
for a private repository on the free plan. A direct push to main is
technically possible; what stops it is §2 rule 2, and what stops it reaching
production is the deploy's ref guard. Turning "require a pull request" into a
setting needs a paid GitHub plan (about $4 per user per month) — a money
decision, recorded in docs/security/soc2-readiness.md CC8.1.
There are no server-side required checks. The commit hook and the pre-push gate
run on the machine the change was written on, so there is no independent
witness: nothing on GitHub shows that they ran, and git commit --no-verify /
git push --no-verify skip them. --no-verify is an emergency hotfix tool
only, under the same conditions as §4 (ADR-0119).