Short answer: with a leaked API key, what to do first is make the leaked value useless. Revoke it at the provider, or, if there is no sign anyone has used it and production depends on it, create a replacement, deploy it and revoke the old one in the same sitting. Deleting the line and pushing a new commit does not fix anything, because the key is still in your history and in every clone. Then confirm your running PHP processes actually loaded the new key, check usage for calls you did not make, and close the path it leaked through before the new key follows it.
Most people do those steps in the opposite order. They see the key in a commit, delete it, push “remove key”, and feel better. The key is still sitting in the previous commit, readable by anyone who can see the repository or who cloned it before you noticed.
Why deleting the line is not the fix
A commit is a snapshot, and git keeps every snapshot. Removing the key from the current file changes the latest snapshot and leaves the earlier ones exactly as they were. The same is true of a frontend bundle cached by a CDN, a CI log, or a paste someone already copied.
GitHub’s own documentation on removing sensitive data from a repository (read September 24, 2026) puts the order plainly: “if the sensitive data you need to remove is a secret (e.g. password/token/credential), as is often the case, then as a first step you need to revoke and/or rotate that secret.” Once the secret is revoked it no longer grants access, and the same page notes that this may resolve the problem without rewriting history at all.
So the job is not to hide the key. The job is to make the key worthless, then make sure your application is running on a new one.
Leaked API key: what to do, minute by minute
The ten minutes below are a working order, not a stopwatch. The point is that each step unblocks the next and none of them waits for a meeting.
Minutes 0 to 2: pick the order, then kill the key
Revoking first causes an outage if production uses that key. Swapping first leaves the old key live for a few minutes. Which risk you accept depends on where the key went:
| Where the key leaked | Order | Why |
|---|---|---|
| Public repository, public gist, frontend bundle, public paste or screenshot | Revoke first, then create and deploy the replacement | Assume someone already has it. A short outage costs less than an open key |
| Usage on your dashboard that you do not recognize | Revoke first | It is being used right now |
| Private repository, internal log, screen share with a few people, no unusual usage | Create the new key, deploy it, verify, then revoke the old one, all in one sitting | Avoids downtime, as long as the gap is minutes and not “after lunch” |
Your provider’s documentation says the same thing in its own words. OpenAI’s Safety best practices guide (read September 24, 2026) says: “If you believe an API key has been exposed, misused, or otherwise compromised, revoke it promptly and replace it with a new key.” When you create the replacement, give it only the permissions this application needs and scope it to one environment, so the next leak, if there is one, reaches less.
Minutes 2 to 5: put the new key where only the server reads it
The new key goes into the server environment, never into a file you commit. Then comes the step most rotation guides skip: making the running application actually use it. PHP applications cache configuration in several places, and a rotated key that the web process has not picked up looks exactly like a rotation that failed.
| Where your app reads the key | What makes the new value take effect |
|---|---|
A .env file loaded by a dotenv library on each request | The next request. Nothing to restart |
Laravel, with php artisan config:cache in your deploy | Laravel’s configuration docs state that once the configuration has been cached, “the .env file will not be loaded.” Run php artisan config:cache again |
A PHP-FPM pool setting such as env[LLM_API_KEY] = ... | Reload PHP-FPM. The service name varies by distribution, for example sudo systemctl reload php8.3-fpm |
| A PHP config file outside git that returns an array | If opcache.validate_timestamps is disabled, reset OPcache or restart PHP. The PHP manual says changes then need opcache_reset(), opcache_invalidate() “or by restarting the Web server” |
| A container environment variable | Recreate the container. A running container keeps the environment it started with |
| Queue workers and other long-running PHP processes | Restart them. They loaded the old value when they booted |
There is one more trap, and it catches people who check their work carefully. Running php -r 'var_dump(getenv("LLM_API_KEY"));' over SSH shows what your shell sees, not what PHP-FPM workers see. The PHP manual’s FPM configuration page documents clear_env with a default of Yes, which “Prevents arbitrary environment variables from reaching FPM worker processes.” Your terminal can show the new key while the website still sends the old one.
Check from inside the web process instead, without ever logging the key. This targets PHP 8.1 or later:
<?php
// Temporary check. Logs a short fingerprint of the key, never the key itself.
// Put it next to the code that calls the provider, load one page, then remove it.
$fingerprint = substr(hash('sha256', (string) getenv('LLM_API_KEY')), 0, 12);
error_log('LLM key fingerprint: ' . $fingerprint);
Then compute the fingerprint you expect, without echoing the key or saving it to shell history:
read -rs NEW_KEY
printf '%s' "$NEW_KEY" | sha256sum | cut -c1-12
unset NEW_KEY
On macOS, replace sha256sum with shasum -a 256. If the two fingerprints match, the web process is on the new key. If they do not, one of the caches in the table above is still holding the old value.
Minutes 5 to 8: check what the key did while it was out
Open your provider’s usage view and look at the window between the leak and the revocation. To find when the leak started, ask git when the key first appeared (see the next step). Look for:
- models your application never calls
- request volume at hours your users are asleep
- usage in a project or environment the key was not meant for
If you find usage you did not make, contact your provider through its official support channel and describe the window. Do not assume charges will be reversed; ask. If the account stores files or other data with the provider, check what the leaked key’s permissions allowed it to read, because a key can expose data as well as spend.
This is also the moment to set a spend limit if you never did. It will not undo anything, but it caps the next incident.
Minutes 8 to 10: find the leak path before it takes the new key too
A key that leaked through a path you have not closed will leak again, and the new one is next. Start with git, using a short unique fragment of the old key rather than the whole value:
# Commits that added or removed the fragment, across all branches
git log -S 'first-8-chars' --all --oneline
# Is an .env file tracked anywhere in the repository?
git ls-files | grep -E '(^|/)\.env$'
Then check the routes that are not git:
- A debug log in a web-readable folder. A log that captured a request payload can carry the key, and a log file the web server will serve to anyone is a public paste. Keeping WP_DEBUG_LOG without displaying errors to visitors includes moving the log somewhere the web server will not serve.
- CI output. A build step that prints its environment puts every secret into the job log.
- The frontend bundle. A key in a
VITE_variable or a public runtime config ends up in client code, which is its own problem with its own fix. - An AI coding assistant. An agent that can read
.envcan paste its contents into a file, a commit or a chat. Review what your assistant can see and do with your keys, and consider constraining what an agent can touch in your repo so.envis out of reach.
After the first ten minutes: history, scanning and habits
Rewriting history is cleanup, not the fix. Once the key is revoked, it cannot be used, whatever the history says. GitHub’s documentation recommends git-filter-repo version 2.47 or later if you do rewrite, and it is candid about the cost: “Rewriting history will change the hashes of the commits that introduced the sensitive data and all commits that came after,” and existing clones and forks keep their copies. The same page says GitHub Support “will only assist in removal of sensitive data in cases where we determine that the risk can’t be mitigated by rotating affected credentials.” For an API key, rotation is that mitigation, which tells you where the priority belongs.
Stop the next one at commit time.
- Add
.envto.gitignorebefore the first real value exists, and commit a.env.examplewith names and placeholder values only. - Add a secret scanner as a pre-commit hook or CI step so a key-shaped string fails the build instead of reaching the remote.
- Set an expiration on new keys where your provider supports it, and keep one key per environment.
- Make “no secrets in the diff” part of review. It belongs on the list of checks before you merge AI-written code, because generated code is quick to inline a value it found in context.
What this runbook cannot do
It cannot make the key unseen. Anyone who copied it before revocation had a working key for that window, and only your provider’s usage records show what they did with it. It cannot reach into forks and clones. And the exact screens differ between providers: where the revoke button lives, whether keys can expire, and how usage is broken down are all in your provider’s own documentation, which beats any general guide on those details.
FAQ
I deleted the key from my code and pushed. Am I safe? No. The key is still in the earlier commit and in every clone made before your fix. Revoke it at the provider; that is the step that actually protects you.
Should I revoke the key first or create the new one first? If the key was public or shows usage you do not recognize, revoke first and accept a short outage. If the exposure was limited and there is no unusual usage, create the new key, deploy it, confirm the app uses it, then revoke the old one immediately.
Why is my app still using the old key after I changed .env?
Something cached the old value. The usual suspects are Laravel’s config cache, a PHP-FPM pool environment that needs a reload, OPcache with timestamp validation disabled, a container that was restarted rather than recreated, or a long-running worker.
The repository is private. Do I still need to rotate? Yes. Everyone with access, past or present, every clone, every CI system and every backup has the key. Private limits who has it; it does not mean nobody does.
Do I have to rewrite git history after rotating? Not to stop the key working; revocation already did that. Rewriting removes the dead value from history for hygiene, at the cost of changed commit hashes and a coordinated re-clone for everyone on the project.

