Rotate first, then deal with the repository
Once a token is in a pushed commit, you have lost control of who has seen it. GitHub's documentation lists where the commit can still be reached after a history rewrite and force push: clones and forks of the repository, cached views on GitHub reached directly by the commit's hash, and pull requests that reference it. None of those is under your control, so the only reliable fix is to make the leaked value useless.
For a Discord bot, open the Bot page in the Developer Portal and use Reset Token. Discord shows a token once, so copy it straight into the bot's environment. For a Telegram bot, BotFather's documented route for a compromised token is the /token command, which generates a new one. For model and other API keys, revoke the key in the provider's dashboard and create a new one.
Rotate everything in the file, not just the token you noticed. A committed .env usually held a model key, a database password and a webhook secret as well. GitHub's guide points out that for a credential, rotating it may be all you need, and the history cleanup becomes housekeeping rather than the fix.
- Discord: Developer Portal, your application, Bot, Reset Token
- Telegram: send /token to @BotFather and choose the bot
- API keys: revoke at the provider, then issue a new key
- Update the bot's environment with the new values and restart it
Deleting the file in a new commit is not enough
A commit that removes the token only changes the latest version. Every earlier commit still holds it, and anyone can check one out. Run git log --all --oneline -- .env to list every commit, on every branch, that touched the file.
Adding the file to .gitignore does not help on its own either. Git's documentation is direct: files already tracked by Git are not affected. To stop tracking a file you keep locally, remove it from the index with git rm --cached .env, add .env to .gitignore, and commit both. The file stays on your disk, Git stops recording it, and the next commit no longer contains it. Earlier commits still do, which is what the next step is for.
- git rm --cached .env removes the file from the index and leaves it on disk
- Add .env to .gitignore in the same commit
- git status should no longer list .env after you save it
Remove it from history with git-filter-repo
GitHub's guide uses git-filter-repo, and needs a version with the --sensitive-data-removal flag, which means 2.47 or later. Work on a fresh clone and keep a backup of the original, because a rewrite changes the hash of every affected commit.
To delete a file from every commit, run git-filter-repo --sensitive-data-removal --invert-paths --path .env. If the file lived at more than one path, add another --path for each one. When the token was pasted into a source file instead, use --replace-text with a file listing the values to replace, so the code stays and only the secret goes.
Then overwrite the remote. GitHub's guide uses git push --force --mirror origin, and notes that pushes to pull request refs fail because GitHub treats them as read-only, and that branch protection may need to be lifted for the push. Everyone with a clone has to start again from the rewritten repository; merging an old clone back in restores the commits you just removed. For cached views and pull requests that still show the old commits, GitHub Support can remove those references.
- Fresh clone, backup kept, git-filter-repo 2.47 or later
- File: --sensitive-data-removal --invert-paths --path .env
- Value inside code: --sensitive-data-removal --replace-text with a list of the secrets
- Force push, ask collaborators to re-clone, and contact GitHub Support for what remains
Set the project up so secrets never reach Git
Discord's quick start says never to check a token into any kind of version control. The way to make that automatic is to keep secrets in environment variables and out of every file Git tracks.
Create .gitignore with .env in it before the first commit, not after. Commit a .env.example with the variable names and empty values, so anyone setting up the bot knows what it needs without seeing your values. In code, read every secret from the environment: process.env in Node, os.environ in Python. For local runs, Node 20.6.0 and later can load the file with node --env-file=.env index.js, and in Python, python-dotenv's load_dotenv reads it. Both leave a variable that is already set in the environment alone, so the same code picks up the real values your host sets.
If you build a Docker image, add .env to .dockerignore as well. That file excludes paths from the build context, so a COPY of the project cannot carry the secrets into an image you later push.
- .gitignore: .env, .env.local and any other file holding real values
- .env.example: names only, committed
- Never log the environment: a debug command that prints configuration publishes the token to whoever reads the log
- Secrets as environment variables, not command-line arguments, which other users of the machine can see in the process list
Push protection is a safety net, not a plan
GitHub's push protection blocks a push that contains a secret it recognises, before the secret reaches the repository. Push protection for users is enabled by default and stops you pushing recognised secrets to public repositories on GitHub. It only knows the formats on GitHub's supported patterns list, and it does nothing for a token pasted into a chat, a screenshot or a private host.
Treat a block as a near miss. Do not bypass it to get the push through: remove the secret from the commit, move it into the environment, and if it was ever exposed anywhere else, rotate it. Then check why it was in a tracked file at all, which is usually a missing .gitignore entry.
Where tokens live on Puranode
Puranode bot hosting sets tokens and API keys as container environment variables in the panel, read with process.env or os.environ, so they are never committed to your repository or baked into an image. Deploy the code from a Git repository or upload it, and the secrets stay in the panel. After a rotation, change the value there and restart the bot from the panel.
Keep the code itself in Git anyway: the container is where the bot runs, not the only copy of it. How AI agent and bot hosting works explains the setup, and if a rotated token still fails, Discord bot keeps going offline covers invalid and reset tokens. The bot hosting page lists the plans.