You wrote the code. You pushed to main. You moved on with your life. Maybe you celebrated with a coffee. Maybe you told someone about it. You felt productive.
Meanwhile, your repo is sitting on GitHub with no branch protection, no CI pipeline, no Dependabot, and no issue templates. It's the digital equivalent of a hospital gown. Technically functional. Open in the back. And everyone can see everything.
"But I'm the only one working on this repo."
Cool. You're also the only person living in your house and you still lock the door. You're the only one driving your car and you still wear a seatbelt. This isn't about other people. This is about protecting you from the most dangerous developer you'll ever work with: yourself at 2 AM.
The Philosophy: Two Rulesets, One Branch
Here's the setup that makes this work. You create TWO separate rulesets for your main branch. Not one. Two. Each with a different job.
"Check Main" is the quality gate. It makes sure your code actually works before it reaches main. It requires pull requests, status checks (lint, typecheck, test, build), and code owner review. It's strict, but it has a VIP list. Your admin account and Claude AI can bypass it. Sometimes you need to get things done fast, and your AI pair programmer shouldn't have to wait for its own PR approval.
"Protect Main" is the bodyguard. It blocks force pushes and restricts branch deletion. Nobody bypasses this one. Not you. Not Claude. Not the CEO. Not God. If something tries to force push to main, the answer is always no. This is the nuclear option protection layer, and it has zero exceptions.
Think of it like a nightclub. "Check Main" is the bouncer at the door who checks your ID and makes sure you're dressed appropriately, but lets the owner skip the line. "Protect Main" is the fire code. The building literally cannot hold more than the maximum capacity. The owner doesn't get to override physics.
Why Two Rulesets Instead of One?
A single ruleset forces you to choose between security and velocity. Two rulesets let you have both. The "Check" ruleset is about code quality and can be bypassed when needed. The "Protect" ruleset is about irreversible damage and can never be bypassed. Different problems, different rules.
Setting Up "Check Main" — The Quality Gate
Go to your repo on GitHub. Navigate to Settings > Rules > Rulesets and create a new branch ruleset. Name it Check Main.
Target
Set the target to Default branch (main/master). This ensures the rule applies wherever your primary branch is.
Bypass List
This is the important part. Add these bypass actors:
- Repository Admin — that's you
- Claude AI — if you're using Claude Code or any AI that pushes via PRs
These actors can push directly to main when the situation calls for it. Your AI assistant shouldn't need to wait for a human to approve a branch it just created. And sometimes you need to do a quick hotfix without the ceremony.
Branch Rules
Enable these:
1. Require a pull request before merging
This is the big one. All changes must go through a PR. Inside the additional settings:
- Required approvals: 1 — Someone (including you) needs to approve
- Require review from Code Owners — checked. If you have a CODEOWNERS file, the right eyes see the right files
- Dismiss stale reviews when new commits are pushed — unchecked for solo devs (you're the only reviewer anyway)
- Require approval of the most recent reviewable push — unchecked (same reason)
- Require conversation resolution before merging — unchecked (useful for teams, overkill for solo)
2. Require status checks to pass
This is where your CI pipeline becomes a gate. Enable it, then configure:
- Require branches to be up to date before merging — checked. No merging stale branches.
Add these four required status checks:
| Check | What It Validates |
|---|---|
lint | Code style, unused imports, potential bugs |
typecheck | TypeScript/type errors caught before runtime |
test | Your test suite actually passes |
build | The project compiles and bundles correctly |
Pro Tip: Name Your CI Jobs to Match
Your CI workflow job names MUST match the status check names exactly. If your workflow has a job called lint-and-format but you add a required check called lint, GitHub will never see it pass. Name your jobs lint, typecheck, test, and build. Clean. Simple. No confusion.
Allowed merge methods: Keep all three enabled (Merge, Squash, Rebase). Different situations call for different strategies. Squash for feature branches with messy commits. Merge for clean histories. Rebase when you want linear history without merge commits.
Setting Up "Protect Main" — The Absolute Law
Create a second ruleset. Name it Protect Main.
Same target: Default branch.
Bypass list: Empty. Nobody. Zero. This is the whole point.
Branch Rules
You only need two things enabled here:
1. Restrict deletions — checked
Nobody gets to delete your main branch. Not accidentally. Not intentionally. Not "just for a second while I reorganize." The answer is no.
2. Block force pushes — checked
Force push rewrites history. On a feature branch, that's fine. On main, it's a war crime. A force push to main can erase commits that other tools, deployments, and integrations depend on. Even if you're solo, your CI, your Dependabot, and your deployment pipeline all trust that main's history is immutable.
Everything else in this ruleset stays unchecked:
- Restrict creations — unchecked
- Restrict updates — unchecked
- Require linear history — unchecked
- Require deployments to succeed — unchecked
- Require signed commits — unchecked
- Require a pull request — unchecked (that's "Check Main"'s job)
- Require status checks — unchecked (same)
- Require code scanning results — unchecked
- Require code quality results — unchecked
Less is more. "Protect Main" does exactly two things, and it does them without exception.
The CI Pipeline That Makes It All Work
Those four status checks won't enforce themselves. You need a GitHub Actions workflow that runs them. Here's the standard template:
1name: CI
2on:
3 push:
4 branches: [main]
5 pull_request:
6 branches: [main]
7
8jobs:
9 lint:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13 - uses: pnpm/action-setup@v4
14 - uses: actions/setup-node@v4
15 with:
16 node-version: 22
17 cache: pnpm
18 - run: pnpm install --frozen-lockfile
19 - run: pnpm lint
20
21 typecheck:
22 runs-on: ubuntu-latest
23 steps:
24 - uses: actions/checkout@v4
25 - uses: pnpm/action-setup@v4
26 - uses: actions/setup-node@v4
27 with:
28 node-version: 22
29 cache: pnpm
30 - run: pnpm install --frozen-lockfile
31 - run: pnpm typecheck
32
33 test:
34 runs-on: ubuntu-latest
35 steps:
36 - uses: actions/checkout@v4
37 - uses: pnpm/action-setup@v4
38 - uses: actions/setup-node@v4
39 with:
40 node-version: 22
41 cache: pnpm
42 - run: pnpm install --frozen-lockfile
43 - run: pnpm test
44
45 build:
46 runs-on: ubuntu-latest
47 needs: [lint, typecheck, test]
48 steps:
49 - uses: actions/checkout@v4
50 - uses: pnpm/action-setup@v4
51 - uses: actions/setup-node@v4
52 with:
53 node-version: 22
54 cache: pnpm
55 - run: pnpm install --frozen-lockfile
56 - run: pnpm buildWhy 4 Separate Jobs?
Running lint, typecheck, and test as separate parallel jobs means faster feedback. If your linting fails in 10 seconds, you know immediately instead of waiting for a 3-minute test suite to finish first. The build job runs last (with needs:) because there's no point building if the other checks already failed.
Notice that lint, typecheck, and test run in parallel. Build depends on all three passing. This gives you maximum speed and clear failure signals.
Adapt for your stack. Using npm? Replace pnpm with npm. Python project? Swap in pip install and pytest. The pattern is the same: separate jobs, clear names, frozen dependencies.
Dependabot: Your Automated Dependency Butler
Dependabot is GitHub's built-in tool for keeping your dependencies from rotting. Left alone, every project slowly turns into a museum of outdated packages with known vulnerabilities. Dependabot stops that.
Enabling Dependabot (Settings Page)
Go to Settings > Code security and analysis. Here's what to enable:
| Setting | Status | Why |
|---|---|---|
| Dependency graph | Enable | Shows you what you depend on (and what depends on you) |
| Dependabot alerts | Enable | Notifies you when a dependency has a known vulnerability |
| Dependabot security updates | Enable | Auto-creates PRs for vulnerable dependencies |
| Grouped security updates | Enable | Batches related security fixes into one PR instead of five |
| Dependabot version updates | Configure | Keeps packages current even without security issues |
| Dependabot malware alerts | Enable | Catches dependencies that have been compromised |
| Automatic dependency submission | Disabled | Only needed for non-standard build systems |
The dependabot.yml Config
Version updates need a config file. Create .github/dependabot.yml in your repo:
1version: 2
2updates:
3 - package-ecosystem: "npm"
4 directory: "/"
5 schedule:
6 interval: "weekly"
7 day: "monday"
8 time: "09:00"
9 timezone: "America/New_York"
10 open-pull-requests-limit: 10
11 reviewers:
12 - "your-github-username"
13 labels:
14 - "dependencies"
15 groups:
16 minor-and-patch:
17 update-types:
18 - "minor"
19 - "patch"
20
21 - package-ecosystem: "github-actions"
22 directory: "/"
23 schedule:
24 interval: "weekly"
25 labels:
26 - "ci"Grouping Is Your Best Friend
Without grouping, Dependabot creates a separate PR for every single dependency update. Monday morning becomes 15 open PRs for minor version bumps. With the groups option, minor and patch updates get batched into a single PR. You review one diff instead of fifteen. Your sanity survives.
Key settings explained:
- schedule.interval: "weekly" — Daily is too noisy, monthly is too slow. Weekly on Monday morning gives you a clean start to the week.
- open-pull-requests-limit: 10 — Prevents Dependabot from flooding you with PRs. If you already have 10 open, it waits.
- groups — The grouping feature batches related updates. Group minor and patch together. Keep major version bumps separate (they're more likely to break things).
- github-actions ecosystem — Don't forget this one. Your CI actions need updates too.
actions/checkout@v3doesn't magically becomeactions/checkout@v4by itself.
Issue Templates: Make Problems Easy to Report
Even on a solo repo, issue templates save time. Six months from now, you'll open an issue that says "fix the thing" and have absolutely no idea what past-you meant. Templates force structure.
Create these files in .github/ISSUE_TEMPLATE/:
bug_report.yml:
1name: Bug Report
2description: Report a bug or unexpected behavior
3labels: ["bug"]
4body:
5 - type: textarea
6 id: description
7 attributes:
8 label: What happened?
9 description: A clear description of the bug
10 validations:
11 required: true
12 - type: textarea
13 id: expected
14 attributes:
15 label: What did you expect?
16 description: What should have happened instead
17 validations:
18 required: true
19 - type: textarea
20 id: reproduce
21 attributes:
22 label: Steps to reproduce
23 description: How can someone else trigger this bug?
24 - type: input
25 id: version
26 attributes:
27 label: Version
28 description: Which version are you running?feature_request.yml:
1name: Feature Request
2description: Suggest a new feature or improvement
3labels: ["enhancement"]
4body:
5 - type: textarea
6 id: problem
7 attributes:
8 label: What problem does this solve?
9 description: Describe the pain point
10 validations:
11 required: true
12 - type: textarea
13 id: solution
14 attributes:
15 label: Proposed solution
16 description: How should this work?
17 validations:
18 required: trueconfig.yml:
1blank_issues_enabled: false
2contact_links:
3 - name: Discussions
4 url: https://github.com/your-org/your-repo/discussions
5 about: Ask questions and share ideasSetting blank_issues_enabled: false forces everyone (including you) to use a template. No more drive-by issues that say "it's broken" with zero context.
"The best time to configure your repo was when you created it. The second best time is right now."
The CODEOWNERS File
If you're using the "Require review from Code Owners" setting in your "Check Main" ruleset, you need a CODEOWNERS file. For a solo dev, it's simple:
# .github/CODEOWNERS
* @your-github-usernameThat's it. One line. You own everything. When a PR touches any file, you're automatically requested as a reviewer. This matters because it triggers the code owner review requirement in your "Check Main" ruleset.
CODEOWNERS With AI Assistants
When Claude AI (or any AI tool) creates a PR, the CODEOWNERS file means you automatically get tagged for review. The AI can bypass the check ruleset to push directly, but the review request still fires. You still see what happened. Transparency without friction.
Putting It All Together
Here's the complete setup sequence, start to finish:
The whole process takes less time than arguing about whether you actually need it. And the first time Dependabot catches a vulnerability, or your CI blocks a bad merge, or "Protect Main" stops you from accidentally force pushing at 1 AM after a long debugging session, you'll wonder why you didn't do this on day one.
Your future self, the one who's debugging something at midnight and accidentally types git push --force origin main, will thank you. Actually, they won't even know it happened. Because "Protect Main" just quietly said no, and they moved on with their life.
That's the whole point.