← Back to list

Our Project Got Hacked — Here’s What We Learned About Secrets in Git

How a security breach taught us that deleting a .env file doesn't actually delete it

Shehan Gamage · 2026-03-26 18:12 · 0 claps · 5.7 min read
#git-security #management-secrets #git-best-practices #gitleaks #git-history
Open on Medium ↗
Wiki topics: BIZ · Business Strategy

Our Project Got Hacked — Here’s What We Learned About Secrets in Git

How a security breach taught us that deleting a .env file doesn't actually delete it

A striking, split-panel infographic visually contrasts the vulnerability and solution for securing sensitive information in a Git repository, featuring an armored data-stealing figure and a secure developer with protective tools.

A striking, split-panel infographic visually contrasts the vulnerability and solution for securing sensitive information in a Git repository, featuring an armored data-stealing figure and a secure developer with protective tools.

Some time ago, our team lead dropped a message in the group chat: “Our project was hacked. All files have been extracted. Including the .git directory."

That last part — “including .git" — is what made it serious. Because .git doesn't just contain the current code. It contains every version of every file that was ever committed. Every password. Every API key. Every service account credential. Even the ones we thought we'd deleted months ago.

Back then, this felt like a rare mistake. But today — in the era of AI-assisted development and Vibe Coding — this risk is actually more common than ever

When you’re generating code quickly using tools like Claude Code, Codex, Cursor, or Copilot, it’s very easy to:

  • Paste credentials temporarily
  • Generate .env files automatically
  • Commit quickly without reviewing
  • Forget secrets were ever added

This is the story of what we found, what we learned, and the two-layer defense system we now use to make sure it never happens again.

The Audit: What We Found

The first thing we did was scan the full git history. Not just the latest code — every commit, across every branch.

The command that changed how I think about git:

git log -p --all -S "password"

This searches every diff in every commit across all branches for the string “password.” And it found things we didn’t expect.

Here’s what our audit uncovered:

  • A MySQL database password — committed in the very first commit, sitting in .env
  • A Google Cloud service account private key — the full RSA key, in a JSON file
  • A Google API key — in both .env and .env.prod
  • A Laravel application key — used for encrypting sessions and cookies
  • 3 email addresses — from commit authors and service accounts
  • Production domains and database hostnames — giving attackers a roadmap

The worst part? Our .gitignore didn't include .env. It only ignored .env.backup and .env.production. The actual .env file had been tracked from day one.

The Misconception That Almost Everyone Has

Here’s what most developers believe:

“I deleted the .env file and added it to .gitignore. The secret is gone."

Wrong.

Git is a permanent ledger. Every commit is a snapshot. When you delete a file, you’re creating a new snapshot without that file — but the old snapshot with the file still exists. Anyone with access to the .git directory can run:

git log -p --all -- '.env'

And see every version of that file that was ever committed. Every password. Every key. Every token. It’s all there.

This is by design. Git is built to never lose data. Which is great for code — and terrible for secrets.

The Two-Layer Defense

After the breach, we implemented a defense system with two layers. The principle is simple: catch secrets before they enter git, and have a safety net if the first layer fails.

Layer 1: Pre-commit Hook (Your First Line of Defense)

A pre-commit hook is a script that runs automatically every time you type git commit. If it detects something that looks like a secret, the commit is blocked. The secret never enters git history.

We use gitleaks — it’s fast, open source, and has good default rules for detecting API keys, passwords, tokens, and private keys.

Setup takes 2 minutes:

# Install the tools
brew install gitleaks pre-commit

Create a file called .pre-commit-config.yaml in your project root:

repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks

Activate it:

pre-commit install

That’s it. Now every commit is scanned. If you accidentally stage a file containing DB_PASSWORD=supersecret123, the commit fails with a clear message telling you exactly what was found and where.

Layer 2: CI Check (The Safety Net)

Pre-commit hooks run locally. But what if a developer skips the hook with --no-verify? What if they haven't set up the hook on their machine?

That’s where the CI check comes in. Add this GitHub Actions workflow, and every push and pull request is scanned before it can be merged:

name: Secret Detection
on: [push, pull_request]
jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

If a secret is detected, the check fails. The PR can’t be merged. No exceptions.

How the Two Layers Work Together

Developer writes code
        |
        v
   git commit
        |
        v
  Pre-commit hook ── Secret found? ──> BLOCKED (never enters git)
        |
      (clean)
        |
        v
   git push
        |
        v
  CI/GitHub Actions ─ Secret found? ──> PR BLOCKED (never merged)
        |
      (clean)
        |
        v
   Merged safely

The pre-commit hook is fast and catches 99% of accidents. The CI check is the safety net that catches the rest.

What To Do When You’ve Already Committed Secrets

If secrets are already in your git history, here’s the playbook:

Step 1: Rotate Everything Immediately

Don’t try to clean up the repo first. Assume every secret in the history is compromised. Change every password, revoke every API key, regenerate every token. Do this now, before anything else.

Step 2: Remove the Files and Fix .gitignore

git rm .env
echo ".env" >> .gitignore
git commit -m "Remove secrets and update .gitignore"

Step 3: Purge from History

Removing the file from the current commit doesn’t remove it from history. You need to rewrite history using a tool like git filter-repo:

pip install git-filter-repo
git filter-repo --path .env --invert-paths

Or BFG Repo Cleaner, which can replace specific strings across all history:

brew install bfg
echo "my-leaked-password" > passwords.txt
bfg --replace-text passwords.txt
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Step 4: Force Push and Re-clone

git push --force --all

Every team member must delete their local clone and re-clone. Their local copies still contain the old history.

Auditing a Repo: The Quick Checklist

If you need to audit a repository for leaked secrets — whether after a breach, before open-sourcing, or during a security review — here are the commands:

# Search for common secret patterns
git log -p --all -S "password"
git log -p --all -S "API_KEY"
git log -p --all -S "secret"
git log -p --all -S "private_key"
git log -p --all -S "token"

# Check env file history
git log -p --all -- '.env' '.env.prod'
# Extract all env values ever committed
git log -p --all -- '.env' | grep '^+[A-Z]' | sort -u
# Find all commit author emails
git log --all --format='%ae' | sort -u
# Automated scan (catches patterns humans miss)
gitleaks detect --source . -v

For each finding, document:

  • The exact value (so the team knows what to rotate)
  • Which file it was in
  • Which commit introduced it
  • Whether it’s still present at HEAD

The .gitignore That Should Have Been There From Day One

This is the minimum for any project:

# Environment files
.env
.env.*
!.env.example
# Credentials
*.pem
*.key
*.p12
*.jks
*credentials*.json
*service-account*.json
# IDE
.idea/
.vscode/
*.swp

The !.env.example line excludes your example template (with placeholder values) from being ignored — that file should be committed as documentation.

Key Takeaways

  1. Deleting a file doesn’t delete it from git history. Once committed, it’s there forever unless you rewrite history.
  2. **.gitignore your secrets before the first commit.* Adding it later only prevents future* commits — the past is already recorded.
  3. Use two layers: pre-commit hooks + CI checks. Hooks catch mistakes locally. CI catches what slips through.
  4. If secrets are leaked, rotate first, clean up second. Don’t waste time purging history while compromised credentials are still active.
  5. Audit your repos proactively. Don’t wait for a breach. Run gitleaks detect --source . -v on your projects today. You might be surprised.

Final Thought

Whether you love Vibe Coding or prefer writing code yourself, one thing is the same — we’re all moving faster than before.

Speed is great. But without guardrails, small mistakes can become security incidents.

Setting up gitleaks + pre-commit takes just 2 minutes and can save hours (or days) of cleanup.

If you’re using AI coding tools regularly, consider adding this to your skillset:

This article was born from a real incident. No customer data was involved — the project was an internal training tool. But the lessons apply to any codebase. Set up gitleaks on your repo today. It takes 2 minutes. The breach we learned from could have been prevented entirely.

Tools mentioned:


메타데이터
post_id
dfab9cf588c6
slug
our-project-got-hacked-heres-what-we-learned-about-secrets-in-git-dfab9cf588c6
url
https://medium.com/@shehangamage55/our-project-got-hacked-heres-what-we-learned-about-secrets-in-git-dfab9cf588c6
canonical_url
https://medium.com/@shehangamage55/our-project-got-hacked-heres-what-we-learned-about-secrets-in-git-dfab9cf588c6
author_url
https://medium.com/@shehangamage55
status
ok
fetched_at
2026-08-18 16:37:06