How to Keep API Keys and Passwords Out of GitHub: .env, .gitignore and Secret Scanning

It happens to almost every developer at least once. You build a project, it finally works, you run git add . and git push... and only later notice that your database password, your email SMTP password or your API key is now sitting in a public GitHub repository. For students, it usually happens with a capstone project pushed the night before the deadline.

Leaked keys are not just embarrassing. Automated bots scan public repositories for credentials, so a leaked cloud or AI API key can be abused and run up charges on your account, and a leaked database password can expose real user data. The good news is that preventing this takes about ten minutes of setup. In this post you will learn a simple habit that works for PHP, Node.js and Python projects: keep secrets in a .env file, keep that file out of Git with .gitignore, and know exactly what to do if a secret slips through anyway.

Every command and code sample below was run on a real test project (Git, Node.js 24, PHP 8.4 and Python 3.13), and the outputs shown are the actual results.

1. Never Hard-Code Secrets in Your Source Code

This is the pattern to avoid:

$conn = new mysqli("localhost", "root", "MyLocalPassw0rd", "shop");
const API_KEY = "sk-live-...";

Once a password is inside a .php, .js or .py file, it goes wherever the code goes: to GitHub, to your groupmates, to the zip you send your adviser, and into every old commit. The fix is to move every secret and every setting that changes between computers (database host, user, password, API keys, SMTP login) into environment variables, usually stored in a file named .env during development.

2. Put Secrets in a .env File, and Commit a .env.example

A .env file is a plain text file with one KEY=value per line. Create it in your project folder:

DB_HOST=localhost
DB_USER=root
DB_PASS=MyLocalPassw0rd
OPENWEATHER_API_KEY=demo-not-a-real-key

Then create a second file, .env.example, with the same keys but no real values. This one is committed, so your groupmates and anyone who downloads your project know which settings they need to fill in:

DB_HOST=localhost
DB_USER=root
DB_PASS=
OPENWEATHER_API_KEY=

Mention it in your README: "Copy .env.example to .env and fill in your own values."

3. Tell Git to Ignore .env

Create a file named .gitignore in the root of your repository:

# Secrets and local settings
.env
.env.*
!.env.example

The .env.* line also covers files like .env.local or .env.production, and the !.env.example line makes an exception so the example file is still committed. You can check whether a file is ignored, and which rule matched it, with git check-ignore -v:

$ git check-ignore -v .env
.gitignore:2:.env	.env

If the command prints nothing, the file is not ignored. Do this check before your first commit, not after.

4. Read the Values in Your Code

Each language has an easy way to load the .env file. Your code then reads the settings by name and stops with a clear message if one is missing.

Node.js (no package needed)

Since Node.js 20.6, you can load a .env file with the built-in --env-file flag, so the popular dotenv package is optional:

// Read settings from environment variables (loaded from .env)
const apiKey = process.env.OPENWEATHER_API_KEY;
if (!apiKey) {
  console.error("Missing OPENWEATHER_API_KEY. Copy .env.example to .env and fill it in.");
  process.exit(1);
}
console.log("DB host:", process.env.DB_HOST);
console.log("API key loaded:", apiKey.slice(0, 4) + "****");
$ node --env-file=.env app.js
DB host: localhost
API key loaded: demo****

$ node app.js
Missing OPENWEATHER_API_KEY. Copy .env.example to .env and fill it in.

Notice that the script never prints the full key. Logging only the first few characters is enough to confirm it loaded.

PHP

For simple KEY=value files you can use PHP's built-in parse_ini_file(). For bigger projects, Composer's vlucas/phpdotenv package is the common choice (Laravel uses it).

<?php
// Load simple KEY=VALUE lines from .env (keep .env outside the web root in production)
$env = parse_ini_file(__DIR__ . '/.env', false, INI_SCANNER_RAW);
if ($env === false) {
    exit("Missing .env file. Copy .env.example to .env and fill it in.\n");
}
$pdoDsn = "mysql:host={$env['DB_HOST']};dbname=shop;charset=utf8mb4";
echo "DSN: $pdoDsn\n";
echo "DB user: {$env['DB_USER']}\n";
$ php config.php
DSN: mysql:host=localhost;dbname=shop;charset=utf8mb4
DB user: root

On a live server, keep the .env file outside the public web folder (for example one level above public_html), or block it in your web server configuration, so nobody can open https://yoursite.com/.env in a browser.

Python

Install python-dotenv with pip install python-dotenv:

import os
from dotenv import load_dotenv

load_dotenv()  # reads .env from the current folder into os.environ

api_key = os.getenv("OPENWEATHER_API_KEY")
if not api_key:
    raise SystemExit("Missing OPENWEATHER_API_KEY. Copy .env.example to .env and fill it in.")
print("DB host:", os.getenv("DB_HOST"))
print("API key loaded:", api_key[:4] + "****")
$ python app.py
DB host: localhost
API key loaded: demo****

Django and Flask projects follow the same idea: read SECRET_KEY, database settings and API keys from the environment, never from a committed settings file.

5. Already Committed Your .env? Here Is What to Do

A common mistake is adding .env to .gitignore after it was committed. Git keeps tracking files it already knows about, so the ignore rule does nothing. In our test project, git check-ignore -v .env printed nothing while .env was still tracked. To stop tracking it but keep your local copy, run:

$ git rm --cached .env
rm '.env'
$ git add .gitignore
$ git commit -m "Stop tracking .env"

After that, git ls-files no longer lists .env. But the secret is still in your history. Anyone can see it in the older commit:

$ git log --oneline -- .env
1777bd6 Stop tracking .env
e4f8298 first commit (oops, includes .env)

$ git show e4f8298:.env
DB_HOST=localhost
DB_USER=root
DB_PASS=MyLocalPassw0rd

So the real fix, in this order, is:

  1. Revoke or change the secret right away. Generate a new API key in the provider's dashboard and delete the old one, and change the database or email password. Assume the old value is already known, because a public repo may have been scanned within minutes.
  2. Remove the file from future commits with git rm --cached and .gitignore, as above.
  3. Optionally clean the history with a tool such as git filter-repo, then force-push. This rewrites history for everyone who cloned the repository, so coordinate with your team. Cleaning history is good hygiene, but it is not a replacement for step 1, since forks and clones may already have the old commits.

6. Let GitHub Catch Mistakes for You

GitHub has two features that act as a safety net. Secret scanning looks for known types of credentials (such as cloud provider and payment API keys) in your repository and alerts you, and push protection blocks a git push that contains a supported secret before it ever reaches GitHub. GitHub turned both on by default for new public repositories in 2024, and you can check or enable them in your repository under Settings, in the Advanced Security page of the "Security and quality" section. See the push protection documentation for details.

Treat these as a seatbelt, not a steering wheel. They work best on known secret formats, such as keys from supported providers. A plain database password in a config file or a home-made token can easily slip past them, so the .env and .gitignore habit still matters most.

7. More Good Habits

  • Review before you commit. Run git status and git diff --staged instead of blindly running git add ..
  • Use different credentials per environment. Your laptop, your test server and your live server should not share the same database password or API key.
  • Give keys the smallest permissions possible and set spending limits or quotas on paid APIs where the provider allows it.
  • Do not paste real keys into AI chats, forums or screenshots. Replace them with placeholders like YOUR_API_KEY before asking for help.
  • Watch your zip files too. If you share a project as a zip (for example to submit to your adviser or upload to a site), make sure the .env file, vendor/ or node_modules/ and local database dumps with real data are not included.
  • Front-end code is public. Anything in browser JavaScript can be read by visitors, even from a .env file processed by your build tool. Keep secret keys on the server side and call the API from there.

Conclusion

Keeping secrets out of GitHub comes down to a few habits: store them in a .env file, ignore that file with .gitignore before the first commit, commit a .env.example for your team, and read the values from environment variables in your code. If a secret does leak, change it first and clean up second. Set this up once in a project template and every future project starts safe.