Handling Secrets and API Keys
Sooner or later your scripts will need a password, an API token, or a database key. Where you put those secrets matters: a key hard-coded into a script and pushed to GitHub is a public leak that anyone can find and abuse.
The Two Golden Rules
- Never hard-code secrets (passwords, tokens, API keys) directly in scripts.
- Never commit secrets to Git. Once pushed, assume the secret is compromised forever, even if you delete it later, it lives in the history.
Incorrect: Secret in the Source
# DON'T DO THIS -- the key is now in your code and your Git history
api_key <- "sk_live_9f83nZ2xQ1pLmR7t"
response <- httr::GET("https://api.example.com/data",
httr::add_headers(Authorization = api_key))
Correct: Read the Secret from the Environment
# DO THIS -- the key lives outside the code
api_key <- Sys.getenv("EXAMPLE_API_KEY")
response <- httr::GET("https://api.example.com/data",
httr::add_headers(Authorization = api_key))
The code is now safe to share; the secret is supplied at runtime by each user’s environment.
Using Environment Variables and Per-User Config
The standard pattern is to store secrets in a per-user config or .env file that stays on your machine and is never committed.
R: .Renviron + Sys.getenv()
Create a file named .Renviron in your home directory (or project root).
R reads it automatically at startup.
# ~/.Renviron -- one KEY=value per line, no quotes, no "export"
EXAMPLE_API_KEY=sk_live_9f83nZ2xQ1pLmR7t
DB_PASSWORD=hunter2
# In your script -- read, never hard-code
key <- Sys.getenv("EXAMPLE_API_KEY")
# Edit .Renviron conveniently with:
usethis::edit_r_environ()
Restart R after editing .Renviron for changes to take effect.
Python: os.environ + a .env File
Store secrets in a .env file and load them with python-dotenv.
# .env -- keep this file out of Git
EXAMPLE_API_KEY=sk_live_9f83nZ2xQ1pLmR7t
DB_PASSWORD=hunter2
import os
from dotenv import load_dotenv
load_dotenv() # reads .env into the environment
api_key = os.environ["EXAMPLE_API_KEY"] # KeyError if missing (fail loud)
db_pw = os.environ.get("DB_PASSWORD") # returns None if missing
Julia: ENV[...]
Julia exposes environment variables through the ENV dictionary.
# Read a variable set in your shell or a .env file
api_key = ENV["EXAMPLE_API_KEY"]
# With a default if it may be unset
db_pw = get(ENV, "DB_PASSWORD", "")
You can populate ENV from a .env file using the DotEnv.jl package (using DotEnv; DotEnv.load!()).
Keep Secret Files Out of Git
Add your secret files to .gitignore so they can never be accidentally committed.
# .gitignore
.env
.Renviron
secrets.yaml
*.pem
Do check git status before committing to confirm no secret files are staged.
Don’t assume a file is safe just because you “meant” to ignore it, verify the .gitignore is working.
What to Do If You Leak a Key
If a secret ever lands in a commit, a shared log, or a screenshot, rotate it immediately:
- Log in to the provider and revoke/regenerate the key. The old value should stop working.
- Update your local
.env/.Renvironwith the new value. - Removing it from Git history (with tools like
git filter-repo) is good hygiene, but rotation is what actually protects you, treat the leaked key as permanently compromised.
Keyring Options
For extra security, store secrets in your operating system’s encrypted credential store instead of a plain-text file:
- R: the
keyringpackage (keyring::key_set("EXAMPLE_API_KEY"),keyring::key_get(...)). - Python: the
keyringlibrary, which talks to macOS Keychain, Windows Credential Locker, or Linux Secret Service.
These avoid having any secret sitting in a readable file at all.