This started as an internal audit.
We were reviewing open source dependencies for a project and ended up falling down a rabbit hole — one repo led to another, a pattern appeared, then another, then another. By the time we'd formally tracked the exercise we were somewhere past a thousand repositories reviewed across the year — a mix of solo projects, startup codebases, open source libraries, and internal tools shared publicly by accident.
What we found wasn't surprising in the sense that these were obscure edge cases. It was surprising in the sense that the same mistakes appeared everywhere — in repos with five stars and repos with five thousand, in projects maintained by one person and projects with fifty contributors. The problems don't discriminate by experience level. They discriminate by whether someone told you about them before you made them.
Here are the seven that showed up most consistently.
Mistake 1 — Secrets in Git History That a .gitignore Can't Remove
This is the most dangerous mistake on the list and the most misunderstood fix.
One leaked API key, an exposed database credential, or a mistakenly committed .env file can lead to massive data breaches, unauthorized access, and even financial loss — and private repositories are not safe from this.
Here's the part most developers get wrong: adding .env to .gitignore after you've already committed it once does nothing. The file is in your git history permanently. Anyone who clones the repo and runs git log can find it. Anyone who forks it takes it with them.
`# This is what a lot of repos look like in their history
git log --all --full-history -- .env
You'll find something like:
commit a3f9d12
Author: Developer Name
Date: Mon Jan 15 2024
"remove .env file" ← too late`
Even if credentials are deleted at a certain point in time, they remain in git history and allow a determined adversary to access those assets — the combination of code sprawl and persistence means secrets in code can be exploited long after developers have moved on from a project.
The actual fix requires rewriting history with git filter-repo — not git filter-branch, which is deprecated and slow — and then rotating every credential that was ever committed. Not just removing it. Rotating it. The key is compromised the moment it touches a commit.
`# Install git-filter-repo (pip install git-filter-repo)
git filter-repo --path .env --invert-paths
Then immediately rotate every key, token, and credential
that appeared in that file. Every single one.`
Prevention is a pre-commit hook with git-secrets or trufflehog that blocks the commit before it ever enters history. Set it up once per machine, never think about it again.
`# Install trufflehog as a pre-commit hook
pip install pre-commit
Add to .pre-commit-config.yaml:
- repo: https://github.com/trufflesecurity/trufflehog
hooks:
- id: trufflehog`
Mistake 2 — No Branch Protection on Main
We found this in the majority of repos with more than one contributor. Main branch sitting completely unprotected — anyone with write access can push directly, force push over someone else's work, or delete the branch entirely.
Using branch protection rules and required reviews significantly reduces risk — without them, developers may unintentionally introduce security risks or break production with a direct push. Technical Ustad
The specific configuration most repos were missing:
GitHub Settings → Branches → Branch protection rules → Add rule
✅ Require a pull request before merging
✅ Require approvals: 1 minimum
✅ Dismiss stale pull request approvals when new commits are pushed
✅ Require status checks to pass before merging
✅ Require branches to be up to date before merging
✅ Do not allow bypassing the above settings
❌ Allow force pushes → OFF
❌ Allow deletions → OFF
The "Do not allow bypassing" setting is the one most people miss. Without it, admins and repo owners can bypass every rule you just set — which makes the protection meaningless in any repo where the lead developer is also the one most likely to push something broken on a Friday afternoon.
Mistake 3 — PRs So Large Nobody Actually Reviews Them
This one doesn't show up in a security scan. It shows up in your commit history as a 4,000-line PR that was approved in eleven minutes.
We saw this pattern repeatedly — a developer works on a feature for two weeks, accumulates changes across fifteen files, and opens a single pull request for all of it. The reviewer is faced with a diff that's physically impossible to review properly in less than an hour. So they scan the obvious parts, leave a comment about a variable name, and approve it. The substantive logic goes unreviewed.
The research on this is unambiguous. Studies on code review effectiveness consistently show that reviewers find progressively fewer defects per line as PR size increases — after about 400 lines of changed code, review quality degrades significantly. Past 1,000 lines, it's essentially ceremonial.
The fix is structural, not a matter of discipline:
Rule: No PR merges if diff exceeds 400 lines of changed code.
Enforcement: GitHub Actions status check that blocks merge if line count exceeded.
# .github/workflows/pr-size-check.yml
name: PR Size Check
on: [pull_request]
jobs:
size-check:
runs-on: ubuntu-latest
steps:
- name: Check PR size
run: |
LINES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}" \
| jq '.additions + .deletions')
if [ "$LINES" -gt 400 ]; then
echo "PR too large: $LINES lines changed. Break it into smaller PRs."
exit 1
fi
Large PRs are a process failure before they're a code quality failure. The automation removes the conversation entirely.
Mistake 4 — README That Describes What the Project Is, Not How to Use It
Technical README failure is less dramatic than a security breach but it's one of the most consistently damaging mistakes in terms of project adoption and maintainability.
The pattern we saw most often: a README that explains what the project does in one paragraph, has a features list, and then drops a single installation command with no context about prerequisites, environment setup, expected output, or what to do when something goes wrong.
A README that actually works contains:
`## Prerequisites
- Node.js 18+ (not 16, not 20 — this project uses fetch natively available in 18)
- PostgreSQL 14+
- An
.envfile based on.env.example(copy it first, then fill in values)
Installation
`\bash
cp .env.example .env
npm install
npm run db:migrate
npm run dev
\`
Expected output
Server running at http://localhost:3000
Database connected: true
Common errors
-
ECONNREFUSED 5432: PostgreSQL isn't running. Start it withbrew services start postgresql -
MODULE_NOT_FOUND: Runnpm installfirst `\`
The "Common errors" section is the one that almost nobody writes and the one that saves every new contributor thirty minutes of debugging. Write it from memory — you know exactly what went wrong the first time you set this up.
Mistake 5 — Issues and PRs With No Template, No Labels, No Triage
Repos with active contributors but zero issue structure. Every issue is a blank text field. No labels. No assignees. No milestones. The result is an issue tracker that looks busy but functions as a to-do list nobody owns.
`# What most repos have:
Issue #47: "it doesn't work"
Issue #48: "bug with login"
Issue #49: "can you add dark mode"
What a maintainable repo has:
[BUG] Login fails with OAuth providers when redirect URI contains query params
Labels: bug, authentication, priority:high
Assignee: @dev-name
Milestone: v2.1.0`
GitHub issue templates take twenty minutes to set up and eliminate the entire category of "it doesn't work" issues by forcing reporters to provide reproduction steps, environment info, and expected versus actual behaviour.
`# .github/ISSUE_TEMPLATE/bug_report.yml
name: Bug Report
description: File a bug report
body:
- type: textarea id: what-happened attributes: label: What happened? description: What did you expect to happen? validations: required: true
- type: textarea id: reproduction attributes: label: Steps to reproduce validations: required: true
- type: dropdown id: version attributes: label: Version options: - Latest - Other (specify in description) validations: required: true`
Mistake 6 — Dependencies Nobody Has Audited Since the Initial Commit
This one compounds silently. A project is started, dependencies are installed, the project ships, and nobody runs npm audit or pip check again for eighteen months. By that point the dependency tree has accumulated multiple known vulnerabilities that have been publicly disclosed, patched in newer versions, and ignored entirely because nobody set up automated alerts.
GitHub is extremely proactive about protecting the security of its users — it has developed tools to help prevent sensitive information from being exposed — but the majority of GitHub-related security incidents are caused by mistakes by its users, not platform failures.
Dependabot takes four minutes to enable and runs automatically:
`# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "automerge-patch"`
The automerge-patch label combined with a GitHub Action that auto-merges Dependabot PRs for patch-level updates removes the entire maintenance burden for minor security fixes. You review minor and major updates manually. Patches merge themselves.
# .github/workflows/dependabot-automerge.yml
name: Dependabot Auto-merge
on: pull_request
jobs:
automerge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Auto-merge patch updates
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Mistake 7 — GitHub Actions Workflows With Excessive Permissions
This is the fastest-growing category of GitHub security mistakes we saw this year and the least understood by the developers making it.
By default, GitHub Actions workflows may receive broader repository permissions than they actually require — one of the most overlooked security features is the permissions setting. Technical Ustad
The default GITHUB_TOKEN in many workflows has write access to the entire repository. A workflow that only needs to run tests and post a comment has no business with write access to deployments, packages, or repository contents. But because the default is permissive, most workflows never have their permissions scoped.
`# What most workflows look like (dangerous default):
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
GITHUB_TOKEN has write access to everything by default
What they should look like:
permissions:
contents: read # Only what checkout needs
checks: write # Only what test reporting needs
pull-requests: write # Only if you're posting PR comments
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test`
The principle is minimal permissions per workflow. A build job needs contents: read. A deployment job needs specific deployment permissions. A release job needs contents: write. Nothing needs blanket write access to the entire repository.
Add this to the top of every workflow file as a default:
permissions: read-all # Deny everything by default, grant explicitly per job
The Pattern Underneath All Seven
Looking across everything we reviewed, the common thread isn't incompetence or carelessness. It's that these mistakes are invisible until they're not. A .env file in git history doesn't announce itself. An unprotected main branch doesn't cause a problem until someone force-pushes on a Friday. Outdated dependencies sit quietly until they're in a CVE.
The fixes for all seven take less than a day to implement across an entire repository. Most of them take twenty minutes. The reason they're not already in every repo isn't that developers don't care — it's that nobody told them what to look for before the problem appeared.
Consider this that conversation.
The Seven-Minute Repo Audit Checklist
Run this on any repository you maintain:
✅ Search git history for .env, secrets, API keys: git log --all -S "SECRET"
✅ Branch protection enabled on main with required reviews
✅ Average PR size under 400 lines — check last 10 PRs
✅ README has prerequisites, installation steps, and common errors section
✅ Issue templates exist in .github/ISSUE_TEMPLATE/
✅ Dependabot enabled and dependency PRs are being reviewed
✅ GitHub Actions workflows have explicit permissions: read-all default
About Exact Solution
We sell professionally refurbished MacBooks and laptops across Europe and the UK — tested, graded, and warranty backed. We also ship code to production and write about what breaks when we do.
https://www.exactsolution.com/collections/laptops
Top comments (0)