---
name: secrets-git-history-purge
description: git-filter-repo to permanently remove committed secrets from git history — secrets are recoverable even in private repos until purged
type: concept
---

# Secrets Committed to Git History — Detection and Permanent Purge

## Why Git History Is Dangerous

`git log`, `git show`, `git blame` all expose history. A secret committed in commit `abc123` and later deleted in commit `def456` is still fully visible via `git show abc123:path/to/.env`.

In private repos this matters because:
- Anyone with repo read access (current or past collaborators, CI tokens) can retrieve it
- GitHub/Bitbucket may have indexed it
- Any clone or fork carries the full history

**Rotation is mandatory immediately; purge removes the attack surface permanently.**

## Detection — Finding the Commits

```bash
# Find commits that introduced .env or secrets files
git log --all --full-history -- .env
git log --all --full-history -- "*.env*"

# Show the actual content of a specific commit's .env
git show abc123:.env

# Search all commits for a specific string (slow on large repos)
git log -p --all | grep -i "POSTGRES_PASSWORD\|SECRET\|API_KEY"
```

## Purge — `git-filter-repo`

`git-filter-repo` is the modern replacement for `git filter-branch` (the old approach is slower, more error-prone, and deprecated by Git maintainers).

```bash
# Install
pip install git-filter-repo
# or: brew install git-filter-repo

# Remove specific files from ALL history
git filter-repo --invert-paths --path .env
git filter-repo --invert-paths --path .env.local
git filter-repo --invert-paths --path config/secrets.yml

# Remove multiple paths in one pass (more efficient)
git filter-repo \
  --invert-paths \
  --path .env \
  --path .env.local \
  --path docker-compose.override.yml
```

`--invert-paths` means "remove these paths" (keep everything else).

## After the Purge

```bash
# Verify the file is gone from history
git log --all --full-history -- .env
# Should return nothing

# Force push ALL branches (required — history was rewritten)
git push --force --all
git push --force --tags

# Notify all collaborators: their local clones are now diverged
# They MUST delete and re-clone — they cannot merge/rebase from old history
```

## Mandatory Steps After Any Secret Exposure

1. **Rotate immediately** — change the secret value before purging history
2. **Purge** — remove from git history
3. **Force push** — push rewritten history to remote
4. **Re-clone** — all team members must delete their local clone and re-clone
5. **Audit access logs** — check if the leaked secret was used during the exposure window
6. **Add to `.gitignore`** — prevent re-committing

## Real Example (Shumiland project — commits eaa200f and ae9a7cf)

```
eaa200f — committed .env with POSTGRES_PASSWORD, PAYLOAD_SECRET
ae9a7cf — committed docker-compose.yml with BINOTEL_HMAC_SECRET
```

Even though `.env` was added to `.gitignore` in a later commit, the secrets remain visible in those two commits until `git-filter-repo` is run.

## Prevention

```gitignore
# .gitignore
.env
.env.*
!.env.example
docker-compose.override.yml
```

Pre-commit hook (add to `.pre-commit-config.yaml`):
```yaml
- repo: https://github.com/Yelp/detect-secrets
  rev: v1.4.0
  hooks:
    - id: detect-secrets
```

Or use `git-secrets` from AWS:
```bash
git secrets --install
git secrets --register-aws
```

**Source:** daily/2026-05-09.md | 2026-05-09
