How to Keep API Keys Out of Scripts and Repos the Right Way
Keep API keys out of scripts and repositories: env files, secret managers, scanning for leaks, narrow scopes and what to do after a key escapes.

The fastest way to turn a small mistake into an incident is an API key in a Git repository. Bots scan public repositories within minutes of a push, and private repositories leak through forks, CI logs and former contributors. The habits that prevent it are simple and consistent. They connect to secrets in CI logs and rotating credentials. The examples assume Bash 5.2, Git 2.47 and gitleaks 8.
Bad and better
# bad: the key is in the file, and in history forever
curl -H "Authorization: Bearer sk_live_EXAMPLEDONOTUSE" https://api.example.test/v1/items
# better: read it at run time
: "${API_TOKEN:?set API_TOKEN, or put it in ~/.config/mytool/token}"
curl -fsS -H "Authorization: Bearer $API_TOKEN" https://api.example.test/v1/itemsThe script itself contains no secret, so it is safe to commit and share. The ${VAR:?message} form stops with a helpful error when the variable is missing, rather than sending an empty header. The key shown in the first line is a made-up value, formatted like a real one to show how recognisable they are to scanners.
Where to keep the value
install -m 600 /dev/null ~/.config/mytool/env
printf 'API_TOKEN=%s\n' "$(pass show api/example)" > ~/.config/mytool/env
set -a; . ~/.config/mytool/env; set +a
API_TOKEN=$(secret-tool lookup service mytool) # Linux keyring
API_TOKEN=$(security find-generic-password -s mytool -w) # macOS keychain
API_TOKEN=$(op read "op://Private/Example API/credential") # password manager CLIA file outside the repository with mode 600 is the simplest option for a single-user machine. A keychain or a password manager CLI is better, since the value is not on disk in plain text and access can be controlled. Servers should receive secrets from the secret store of the platform or from a systemd credential, see environment variable leaks. For tools that support it, the _FILE convention (API_TOKEN_FILE=/run/secrets/token) keeps the value out of the environment. Do not put secrets in command-line arguments, which show up in the process list, as in secrets in the process list.
Repository hygiene
printf '.env\n.env.*\n!.env.example\n*.pem\n*.key\n' >> .gitignore
cat > .env.example <<'EOF'
API_TOKEN=replace-me
API_BASE=https://api.example.test
EOF
git check-ignore -v .env.gitignore:1:.env .env
Ignore the real files and commit an example with fake values so new contributors know what to set. Check that the ignore works before the first commit; a file already tracked stays tracked after being added to .gitignore and must be removed with git rm --cached. Never commit a "temporary" secret, because history keeps it.
Catch mistakes before they leave
cat > .git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
exec gitleaks protect --staged --redact --verbose
EOF
chmod +x .git/hooks/pre-commit
gitleaks detect --source . --redactFinding: Authorization: Bearer REDACTED
RuleID: generic-api-key
File: deploy.sh
Line: 14
A pre-commit hook blocks the commit when a staged change contains something that looks like a secret; running the same scanner in CI backs it up when someone skips the hook. gitleaks detect scans the full history for old leaks. The tools are covered in gitleaks and trufflehog, and finding existing secrets in scanning a repository. Many hosts also offer push protection that rejects pushes with known token formats.
Scope and lifetime
Give each key the narrowest permission that works (read-only where possible, one project, one resource), one key per tool or environment, an expiry date where the provider allows, and an owner who gets the alert. A restricted key limits what a leak costs. Prefer short-lived tokens from OAuth or workload identity over long-lived keys, see OAuth from the command line.
When a key leaks
1. Revoke or rotate the key at the provider (do this first)
2. Check the provider's usage or audit logs for activity since the leak
3. Replace the key everywhere it is used
4. Remove it from the repository and history (git filter-repo), then force-push and tell collaborators
5. Find out how it leaked and fix that
Deleting the file and committing does nothing: the key remains in every earlier commit, in forks and in clones. History rewriting cleans your repository, not copies elsewhere, so treat the key as compromised regardless. The order matters: revoke before cleaning up.
Why keys leak, in practice
Most leaks are not attacks on a vault. They are ordinary working habits. A key pasted into a script to try something, committed with the rest. A configuration file copied into an example repository. A debugging session that prints the environment into a log. A screenshot of a terminal in a bug report. A backup of a laptop that contains a dotfile with a token. Each one is a moment of convenience, and the defences are mostly about making the safe path as easy as the unsafe one, so that nobody has to choose between convenience and care.
Automated scanners make the consequences fast. Public repositories are crawled continuously for patterns that match known key formats, and providers run their own scanners and revoke keys they find, which is helpful and also tells you the key was visible. Assume that any secret that touched a public place for even a minute has been seen. Private repositories are safer, but they are copied to laptops, forks and CI systems, so a secret there is still wider than the person who typed it.
Designing scripts that never need the secret in the file
Read secrets from the environment or a file at run time, and fail clearly if they are missing. Accept the location of the secret as a setting, such as --token-file, not the secret itself, so that the command line, which is visible in the process list and shell history, never carries it. Prefer short-lived credentials obtained at run time, from an identity provider or a cloud role, to long-lived keys stored anywhere. A script that exchanges its identity for a token that expires in an hour has nothing worth stealing in its repository or its logs.
Avoid printing the value. A debug line that echoes the headers of a request, or an error message that includes the full URL with a token in the query string, puts the key in logs. Use headers rather than query parameters for credentials where the API allows, since URLs are logged by proxies and servers in many places, and redact when logging.
Shared secrets in teams
When several people need the same credential, use a team vault or the platform's secret store instead of chat messages and spreadsheets. Give each service its own key so that revoking one does not stop everything, and give each person their own where the provider supports it, so that offboarding removes access without a rotation of shared values. Keep an inventory of keys, their scopes, their owners and their rotation dates; a key nobody can attribute is a key nobody dares delete, and it lives forever.
In CI, store secrets in the platform's encrypted settings and expose them only to the jobs and branches that need them. Do not echo them, and do not pass them to steps from untrusted forks. Rotating on a calendar, for example every ninety days, keeps the process practised so that an emergency rotation is routine.
Cleaning up after a mistake
If a key has been committed, rotate it before anything else, because history rewriting does not reach every copy. After rotation, rewrite the history to remove the value with a tool designed for it, push the cleaned branches and ask collaborators to re-clone, since old clones still contain the secret. Search for other places the key could be: issue comments, pull request descriptions, CI logs, chat messages and artifacts. Write a short note on how it happened and what changed, whether a hook, a scanner or a different workflow, so the same mistake becomes harder next time.
Where this does not apply
Public client identifiers, publishable keys (such as a browser-side map key restricted by domain) and test keys are not secrets, though restricting them is still wise. Secrets that must live on a device, such as a mobile app, cannot be hidden; design the API so that the client key grants little. See the GitHub secret scanning documentation and the gitleaks repository.


