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:
Go to the provider’s dashboard and revoke the key. Create a new one.
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.
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:
--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:
That drops .env from every commit. To replace a literal value wherever it appears instead of removing a file, give it a replacements file:
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:
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:
A quicker grep over tracked files, for the patterns that matter most, without installing anything:
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:
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_globaland add.env,.env.*,*.pem,*.p8to it. A per-repository.gitignoreis forgotten; the global one is not.Commit an example, not the real thing.
.env.examplewith placeholder values documents what the app needs without holding anything.Scan before commit. A pre-commit hook running
gitleaks protect --stagedcatches 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
Developer Vault: encrypted storage for keys, tokens, and
.envvaluesProjects: where credential files and hardcoded secrets get flagged
