Guide

You committed an API key. What to do in the next ten minutes.

Rotate first, rewrite history second, then make sure it cannot happen in the next repository either.

Rotate first, rewrite history second, then make sure it cannot happen in the next repository either.

Rotate first, rewrite history second, then make sure it cannot happen in the next repository either.

Rotate the key before anything else

The moment a key is in a commit, treat it as leaked. If the repository is public or was pushed anywhere shared, scanners find keys within minutes, and even a private repository has clones, CI logs, and forks you do not control. Rewriting history does not un-leak anything.

So, first:

  1. Go to the provider’s dashboard and revoke the key. Create a new one.

  2. Put the new value where the app reads it at runtime (environment variable, CI secret, secret manager). Not in the file you are about to clean.

  3. Check the provider’s usage log for the old key. If anything looks unfamiliar, escalate now rather than after cleanup.

Only after the old key is dead is the rest of this guide about hygiene instead of damage.

Remove it from the working tree and the next commit

If the secret is in a tracked file that should never have been tracked (.env, config/secrets.yml), stop tracking it and ignore it:

git rm --cached .env echo ".env" >> .gitignore git commit -m "Stop tracking .env"

--cached removes the file from the index but leaves it on disk. If the secret is a literal inside source, replace it with a lookup (process.env.STRIPE_KEY, ProcessInfo.processInfo.environment["STRIPE_KEY"]) and commit that.

This fixes the tip of the branch. The old commits still contain the value.

Remove it from history

Only do this if the repository is small enough and shared with few enough people that a rewrite is manageable, because every clone will have to be re-cloned or hard reset afterwards. For a solo project it is five minutes. For a team repository, tell everyone first.

git filter-repo is the tool Git’s own documentation recommends over filter-branch:

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

That drops .env from every commit. To replace a literal value wherever it appears instead of removing a file, give it a replacements file:

printf 'sk-live-abc123==>REDACTED ' > /tmp/replacements.txt git filter-repo --replace-text /tmp/replacements.txt

filter-repo refuses to run on a repository that is not a fresh clone unless you pass --force, which is a feature: clone, rewrite, verify, then push. After the rewrite, force push every branch and tag and ask collaborators to re-clone:

git push --force --all git push --force --tags

Then check the platform. GitHub, for example, keeps unreachable commits reachable by hash until its garbage collection runs and may need a support request to purge cached views of a public repository. Read the platform’s “removing sensitive data” page for the current steps.

If a rewrite is not realistic, you already did the part that matters: the key is rotated. The old value in history is now a dead string.

Find the others across your repositories

One leaked key usually means a habit. Scan everything you have locally before the next one ships. gitleaks is a common choice:

brew install gitleaks for repo in ~/Developer/*/; do echo "== $repo" gitleaks git "$repo" --no-banner 2>/dev/null | tail -3 done

A quicker grep over tracked files, for the patterns that matter most, without installing anything:

git grep -nE 'sk-(proj|live|test)-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----'

Run it in each repository, or with git grep replaced by grep -rnE to include untracked files. Also list credential files that sit in project folders, tracked or not:

find ~/Developer -maxdepth 5 \( -name '.env*' -o -name '*.pem' -o -name '*.p8' -o -name '*.p12' -o -name 'service-account.json' \) -not -path '*/node_modules/*'

Every hit is a file that a clone will not bring back and a backup might expose.

Keep keys out of the working tree

Three habits prevent most repeats:

  • Ignore the file globally. git config --global core.excludesfile ~/.gitignore_global and add .env, .env.*, *.pem, *.p8 to it. A per-repository .gitignore is forgotten; the global one is not.

  • Commit an example, not the real thing. .env.example with placeholder values documents what the app needs without holding anything.

  • Scan before commit. A pre-commit hook running gitleaks protect --staged catches the next one before it is a commit at all.

And decide where the real values live. A loose .env in the project folder is readable by every process you run from that folder, including every dependency’s postinstall script and every coding agent with shell access. Options that are better: the macOS Keychain through security add-generic-password, your team’s secret manager, or an encrypted local vault that unlocks when you need a value and locks again.

Doing this in Room Service

Room Service’s Projects workspace runs a Project Health pass on every repository it finds. Two of its checks are about this guide. One lists credential files inside the project: .env and its variants, .npmrc, .netrc, .pem, .key, .p8, .p12, .mobileprovision, .jks, service-account.json, client_secret.json, and similar names. The other scans tracked source files for hardcoded secrets: private key blocks, OpenAI, GitHub, AWS, Google, and Slack token shapes, bearer strings, and api_key = "..." style assignments. Matches are shown as masked previews with the file and line, never the full value, and there is no cleanup button: the fix stays yours.

Developer Vault is the place to put the values afterwards. Import a .env, .env.*, or .p8 file and each line becomes an encrypted item (AES-256-GCM, key derived from your password with PBKDF2, Touch ID optional). Copy a value and the clipboard clears after 45 seconds. The vault has no MCP scope, so an agent connected to Room Service cannot read it, and its folder is excluded from every cleanup.

Credential file names and secret previews are hidden from the CLI and from agents by default. roomservice project --show-sensitive reveals them on the command line; over MCP they stay hidden until you turn on the projects.sensitive scope, which is off by default. The Projects workspace is Pro. In the vault, creating items, importing files, and restoring backups need Pro.

Related

Frequently Asked Questions

I pushed an API key to GitHub. What is the first thing to do?

How do I remove a file from all Git history?

Is it enough to delete the file and commit?

How do I find secrets in all my repositories on a Mac?

Where should I keep .env values instead?

Can an AI agent read my Developer Vault through Room Service?

More guides