Putting an API key in a repository takes about four seconds, and getting it back out can eat an afternoon. Keep the key in an environment variable and list the file that holds it in .gitignore before your first commit. Workflows get the value through GitHub Secrets.
The order matters because the gitignore documentation in the Git manual states that files already tracked by Git are not affected by the rule.
A key that reaches a commit stays exposed until it is revoked, and deleting the file does not change that. GitHub Support purges cached copies of leaked data only when rotating the credential cannot mitigate the risk (GitHub Docs).
How to hide an API key in GitHub with a .env file

The key lives in an environment variable, and the file holding it stays out of Git. A placeholder template goes into the repo instead, so collaborators know what to fill in. For a personal project or a small app, that’s the whole job.
The scale of the problem is bigger than most people assume. GitHub counted more than 39 million leaked secrets on its platform in 2024 (GitHub blog, 2025), and a key pasted straight into a source file is the most direct way to add to that number.
- Create a file named .env in the project root and add a line like API\_KEY=your\_key\_here.
- Add .env to .gitignore before the first commit.
- Create .env.example with the same variable names and fake values, and commit that one instead.
- Load the variable in code rather than writing the key into a source file.
- Run git status and confirm .env is not listed.
Step 2 is the one people get wrong. A .gitignore file only stops Git from picking up files it is not tracking yet, so the rule has to exist before the commit that would otherwise include the key.
Before you push, run a git status check. An ignored .env never shows up in the output.
Reading the key in Node.js
On Node.js 20.6.0 and later, start the app with node –env-file=.env index.js and read process.env.API\_KEY. No extra package needed, and I’d skip dotenv entirely on anything recent.
Older versions still need it. Install dotenv, call require(“dotenv”).config() at the top of the entry file, and read the same process.env.API\_KEY.
Reading the key in Python
The python-dotenv package reads the .env file and copies its values into os.environ. Install it with pip install python-dotenv and call load\_dotenv() once at startup. After that, os.environ[“API\_KEY”] gives you the value.
Bracket access raises a KeyError when the variable is missing. That beats quietly sending an empty key to the API and debugging a 401 for an hour.
Why .gitignore does not hide a key that was already committed
.gitignore only affects files Git is not tracking yet. The gitignore page of the Git manual says it plainly: files already tracked by Git are not affected.
So if .env went into a commit before the ignore rule existed, adding the rule changes nothing. Git keeps tracking the file and keeps recording every edit to it.
Untracking the file
- Run git rm –cached .env. The file stays on your disk and leaves the index.
- Confirm .env is listed in .gitignore, then commit the removal.
The git rm command with the –cached flag is the one the Git documentation points to for stopping tracking of a file that is currently tracked.
Checking why an ignore rule is not matching
Run git check-ignore -v .env. It prints the exact rule and file that match, or nothing if no rule applies.
This pattern set works for most projects:
- .env
- .env.\*
- !.env.example
The negation line goes last, otherwise the broader pattern wins and your template stays ignored too.
History keeps the key
Untracking does not touch earlier commits. The key still sits in the commit history, readable by anyone with access to the repository.
Treat it as exposed. Revoke it and issue a new one. Rewriting history can wait until after that.
How to store an API key in GitHub Secrets for GitHub Actions
GitHub Secrets stores credentials as encrypted values that a workflow reads through the secrets context. The key never appears in the repository files, and you can scope it as narrowly as one repository or as widely as an organization, with environments in between.
To add one, open the repository, go to Settings, then Secrets and variables, then Actions, and create a new repository secret. On a personal account repository you must be a repository collaborator, and on an organization repository you need write access. Environment secrets set a higher bar: you must be the repository owner on a personal account, or have admin access on an organization repository (GitHub Docs).
These values are only available to GitHub Actions workflows, so a local run still needs its own .env. In the workflow file, pass the secret to the step as an environment variable with env: API\_KEY: ${{ secrets.API\_KEY }}.
GitHub Docs recommends environment variables or STDIN over command-line arguments, because other users on a machine can see command-line processes.
Repository, environment and organization secrets
A repository secret is available to the workflows in that one repository. Environment secrets are tied to a named environment such as production, and a job reads them only when it starts and references that environment. Organization secrets are shared across whichever repositories you select, and they’re read when a workflow run is queued.
Private repositories on GitHub Free cannot access organization-level secrets.
Limits and naming rules
The limits, per the GitHub Docs secrets reference as of October 2026:
- Up to 100 secrets per repository
- 100 per environment
- As many as 1,000 organization secrets
- Each secret can be 48 KB at most
If a repository is assigned more than 100 organization secrets, a workflow can only use the first 100 sorted alphabetically by name. Names take letters, numbers, and underscores only, cannot start with GITHUB\_ or a number, and are stored in uppercase.
Avoid structured data such as JSON as a secret value. GitHub’s log redaction works less reliably on it.
Forks add one more failure mode. Workflows triggered by pull requests from a fork receive no secrets except a read-only GITHUB\_TOKEN, and GitHub treats Dependabot pull requests the same way (GitHub Docs).
A test job that needs your key fails on those pull requests. That is by design.
Which method fits which project: .env, GitHub Secrets or a secrets manager
A .env file covers local development, and GitHub Secrets covers anything a workflow needs. A secrets manager only makes sense when several services or people share keys that need central rotation. Most small projects never get past the first two.
| Method | Where the key lives | Rotation | Main failure mode |
|---|---|---|---|
| .env file | Local disk, git-ignored | Edit the file on each machine | Committed or copied by accident |
| GitHub Secrets | GitHub, read by workflows | Update by hand in Settings | Workflows only, nothing for fork pull requests |
| Secrets manager | Central service | Can run on a schedule | Extra cost and setup |
Pros and cons of each method
.env file
It costs nothing and works offline, and every common language runtime supports it. The catch is that every developer keeps a separate copy, and one gitignore mistake leaks it.
GitHub Secrets
Values are stored encrypted and redacted in logs, and there’s no extra account to set up. You can scope them by repository or environment. They don’t work outside workflows, though, so local runs still need a .env, and there is no built-in rotation.
Secrets manager
You get one source for many services, plus a central audit trail and scheduled rotation. You also pay per secret and take on another system to run and secure.
AWS Secrets Manager charges $0.40 per secret per month (AWS pricing page), so ten separate keys cost $4.00 a month before API call charges. HashiCorp Vault covers the same job for teams that prefer to host it themselves.
Decision rule
Skip the vault for a solo project. A single repository with one workflow and a handful of keys sits comfortably inside GitHub Secrets plus a local .env.
A secrets manager starts paying off when keys feed several services through a deployment pipeline and nobody can say who last rotated them. Until then it is overhead.
How to block API keys from reaching GitHub in the first place
The cheapest place to catch a key is on your own machine, before the commit exists. Push protection on GitHub is the next net, and a history scan picks up whatever got through earlier. Each one leaves a gap that the others cover.
| Layer | Runs when | Main gap |
|---|---|---|
| Pre-commit hook (gitleaks) | At git commit | Local only, can be skipped |
| Push protection | At git push | Unsupported key formats |
| History scan (gitleaks, TruffleHog) | On demand or in CI | Runs after the leak |
Secret scanning and push protection on GitHub
Secret scanning for supported patterns is free on public repositories. Push protection for users is on by default and blocks your pushes of supported secrets to public repositories. If you bypass it, no alert is generated unless push protection is also enabled on the repository (GitHub Docs).
Private repositories are different. Both features need GitHub’s paid security tooling, and GitHub split Advanced Security into standalone Secret Protection and Code Security products in 2025 (GitHub blog).
Providers get in on it too. Through the secret scanning partnership program, service providers get GitHub to scan public repositories and npm packages for their own token formats.
GitHub puts the number of secrets prevented from leaking in 2024 at 4.4 million (GitHub Secret Protection page).
The documented gaps are worth knowing. If the scan on a push times out, GitHub still scans the commits after the push, so the block can arrive late as an alert instead. A fork of a repository with push protection enabled also doesn’t get it enabled by default.
A key format the scanner does not recognize passes straight through. Organizations can add custom patterns for proprietary formats, but a personal repository cannot.
Pre-commit scanners on your machine
The pre-commit framework runs gitleaks before each commit is created, which catches the key while it still exists only on your disk.
Add the gitleaks hook to .pre-commit-config.yaml, then run pre-commit install once in every clone. If you wire gitleaks into a hook yourself instead, pass the –staged flag.
The weak spot is that the hook lives in your local clone. Prefixing a commit with SKIP=gitleaks switches it off, and a teammate who never ran pre-commit install gets no protection at all (gitleaks README).
Gitleaks also ships as a GitHub Action (gitleaks-action@v2), which fits into a continuous integration job that scans every push. detect-secrets plugs into pre-commit the same way, and TruffleHog is the usual pick for combing through history that already exists.
What to do when an API key is already on GitHub
Revoke the key first and rewrite history second. A rewrite never undoes the exposure, so the old key has to stop working before anything else happens.
GitHub Docs states the limit directly. After a rewrite and force push, the old commits can still be reached in clones and forks, directly by SHA-1 hash in cached views, and through pull requests that reference them.
Reverting is no shortcut either. A git revert adds a new commit that undoes the change and leaves the original commit in place.
- Revoke the exposed key at the provider, create a replacement, and check the usage logs for calls you did not make.
- Store the new key in your .env file and in GitHub Secrets.
- Merge or close open pull requests, because GitHub recommends doing so before removing files.
- Rewrite history locally with git filter-repo.
- Force-push the rewritten history to GitHub.
- Contact GitHub Support to purge cached views and pull request references.
- Ask every collaborator to delete their old clone and clone again.
Rewrite history with git filter-repo
To remove a whole file, run git filter-repo –sensitive-data-removal –invert-paths –path .env. To replace only the value, use git filter-repo –sensitive-data-removal –replace-text ../passwords.txt. Then push the result with git push –force –mirror origin.
The –sensitive-data-removal flag needs git filter-repo 2.47 or later (GitHub Docs, as of October 2026).
BFG Repo-Cleaner does the same job with bfg –delete-files .env or bfg –replace-text passwords.txt. It protects the current HEAD commit by default, so the key must already be gone from your latest commit or BFG leaves it in place.
GitHub’s Enterprise Server 3.5 documentation lists BFG as a faster, simpler alternative, while the current GitHub.com page walks through git filter-repo. Pick git filter-repo. It is the documented route today and it has the dedicated flag for this exact situation.
What stays exposed after the rewrite
- Clones and forks keep the old commits, and nobody can remove them from someone else’s copy
- Cached views stay reachable by commit hash and through pull request references until Support purges them
- A teammate with a pre-rewrite clone who pulls and pushes brings the data straight back
- Diffs of closed pull requests stop rendering
GitHub Support only helps with removal when it decides rotating the credentials cannot mitigate the risk (GitHub Docs). For an API key that has been revoked, expect the answer to be no.
When hiding an API key in GitHub does not work
Keeping a key out of the repository does nothing for a key that ships to the browser. Whatever the page or the app bundle contains, every visitor can read it.
What the browser can always read
Vite’s documentation says variables prefixed with VITE\_ are exposed to client source code and end up in the client bundle, so they should hold nothing sensitive. The values are replaced statically at build time.
Anything compiled into front-end code is delivered to every visitor. A .gitignore rule protects the repository, not the built JavaScript on your host.
The Network tab shows the rest, since any request URL or header carrying the key is visible. Rotating a leaked client key also means rebuilding and redeploying the app.
| Key kind | Safe in the browser | What protects it |
|---|---|---|
| Secret key with billing or write access | No | Server-side proxy |
| Restricted key built for browsers (such as a Maps JavaScript key) | Yes | Referrer and API restrictions, quota caps |
| Build-time variable (Vite VITE\_ prefix) | Public by definition | Nothing, treat as published |
Keeping the key on a server
The browser calls your own endpoint, and that endpoint adds the key and calls the third-party API. It can run as a serverless function or a small back-end service, with the key stored in its environment variables or in GitHub Secrets for the deploy.
Give the endpoint its own authentication or rate limits. Without them, anyone can spend your quota through the proxy.
GitHub Pages serves static files only, so a key placed there ships to the browser. The function has to live elsewhere.
Restricting a key that has to ship
Some keys must be public. Google’s Maps Platform team publishes a Street View image on its blog with a visible key, then limits that key with HTTP referrer restrictions, API restrictions, digitally signed URLs, and a zero quota for unsigned requests (Google Maps Platform blog).
Referrer checks stop casual reuse on other websites. A script outside a browser can send a forged referrer, so they slow abuse down without ending it.
Cap the quota and restrict the key to the single API it calls. Any token should carry only the scopes the job needs, and a personal access token with broad scope never belongs in client code.
FAQ about hiding an API key in GitHub
Is a private repository enough to protect an API key?
No. Every collaborator, fork, and clone still holds the key, and a compromised account or token exposes it.
Visibility can also change later, so the key committed today may sit in a public repository next year.
Does deleting the repository remove a leaked API key?
No. Forks and clones keep the commits, and GitHub cannot remove data from other people’s copies (GitHub Docs).
The key keeps working until you revoke it at the provider, so do that first.
What is the difference between secrets and variables in GitHub Actions?
Mostly visibility. Variables are stored as plaintext, so anyone with repository access can read them, which suits non-sensitive values such as a region name or a feature flag. Secrets are encrypted and masked in logs.
Workflows read them through different contexts: secrets for credentials, vars for configuration.
Can I bypass push protection when it blocks a push?
Yes. By default, anyone with write access can bypass it by choosing a reason: used in tests, false positive, or I’ll fix it later.
The first two create a closed alert, the third an open one (GitHub Docs). Delegated bypass restricts the decision to designated reviewers.
How do I hide an API key in an Android project?
Put the key in local.properties and keep that file out of Git. The app-level build.gradle reads it, and the value reaches your code through BuildConfig.
The compiled app still contains the key and anyone can extract it from the APK. Keys with billing or write access belong on a server.
How Often to Rotate a Hidden API Key
Hiding an API key in GitHub keeps it from casual discovery, but only rotation limits the damage of a leak. Rotate immediately after any suspected exposure, and on a fixed schedule otherwise.
Rotate the moment a key reaches a commit or a client bundle, and again when a collaborator with access leaves. Otherwise follow whatever schedule your provider or secrets manager supports.
GitHub’s own removal process treats rotation as the control that ends an exposure, which is why a schedule beats cleanup.
Every rotation means updating each local .env copy and each GitHub Secrets entry, then rebuilding any client app that embeds the key. Short-lived credentials from token-based authentication cut that cost where the provider offers them.
Checked in October 2026. If GitHub changes which plans include scanning for private repositories, the schedule advice changes with it.
- How to Run a React App in VS Code Step by Step - October 23, 2026
- Transactional or marketing email: which messages your app should send itself - October 23, 2026
- How Software Teams Write SOPs for Releases and Deployments - October 22, 2026




