Secrets, .env and .gitignore
Intermediate Core Git Git CLI GitLab UI GitHub UI
Why this matters
Git is designed to remember everything forever and to copy itself to every colleague's laptop and every build machine. That is exactly wrong for a password. The good news is that keeping secrets out is a habit, not a technology, and the habit is three lines long.
What counts as a secret
| Secret: never in Git | Configuration: fine in Git |
|---|---|
| Passwords, API keys, access tokens | Which port the app listens on |
Private keys (.pem, id_ed25519) |
Feature flags and defaults |
| Database connection strings with credentials | The names of the settings that exist |
| Signing certificates | Timeouts, limits, log levels |
.env files with real values |
.env.example with empty values |
The test: if it appeared on a public page, would you have to change something? If yes, it is a secret. A port number is not; a token is; a database host name is a judgement call and usually belongs with the secrets.
Note the last row. The list of settings is useful to everyone and belongs in the repository; only the values are secret.
The example-file pattern
Two files with almost the same name, and the whole convention rests on the difference:
DATABASE_URL=
TRAILGUIDE_API_KEY=
LOG_LEVEL=infoDATABASE_URL=postgres://localhost/trailguide
TRAILGUIDE_API_KEY=sk-live-9f8e7d6c5b4a
LOG_LEVEL=debugAnd one line in .gitignore, which the playground already has:
# Secrets and local settings — never commit these
.env
*.localA new colleague copies .env.example to .env, fills in the values from the team's password manager, and is running in five minutes. Nobody had to send a token in a chat message, and Git never saw one.
Git enforces the rule for you:
$ git status --shortNothing. The file exists on disk and Git looks away:
$ git check-ignore -v .env.gitignore:7:.env .envAnd if you try to add it anyway:
$ git add .envThe following paths are ignored by one of your .gitignore files:
.env
hint: Use -f if you really want to add them.
hint: Disable this message with "git config set advice.addIgnoredFile false"Where the real values live
| Place | For |
|---|---|
Your machine, in an ignored .env |
Local development |
| The team's password manager | Sharing between people |
| GitLab: Settings → CI/CD → Variables (mark Masked and Protected) | Values the pipeline needs |
| GitHub: Settings → Secrets and variables → Actions | The same on GitHub |
| The platform's environment settings, or a dedicated vault | Values the running application needs in production |
The pipeline case is the one you will meet as a non-developer, because a documentation deployment often needs a token. Both platforms inject those values as environment variables at job time and mask them in the logs, so they never appear in the repository or the output.
How to do it
$ cp .env.example .env # then fill in the values
$ git check-ignore -v .env # prove Git is ignoring it
$ git status --short # .env must not appearBefore any commit that touches configuration, git diff --staged and read it. A secret in a diff is much easier to catch than a secret in history.
Ignored files appear greyed out in the Explorer, which is a useful visual check: your .env should be grey. If it is not, it is not ignored.
Right-click an untracked file in Source Control → Add to .gitignore appends the right line. Extensions such as dotenv highlight .env syntax without sending anything anywhere.
Ignored files are shown in a different colour (olive by default) in the Project view. Right-click an unversioned file → Git → Add to .gitignore does the same as VS Code.
IntelliJ also warns when a commit includes a file that looks like it contains credentials, which is worth leaving enabled.
Settings → CI/CD → Variables holds values for pipelines. For each variable, tick:
- Masked, so the value is replaced by
[MASKED]in job logs. - Protected, so it is available only to pipelines on protected branches, which keeps it away from feature branches anyone can push.
GitLab also runs secret detection on pushes and merge requests in many plans, and shows a security finding when a credential-looking string appears in a diff. Treat any such alert as real until proven otherwise.
Settings → Secrets and variables → Actions, with Repository secrets for values and Variables for non-secret configuration. Secrets are write-only: once saved, nobody can read them back through the interface, only update or delete them.
GitHub's secret scanning watches pushes for known credential formats and can block the push outright (push protection). If it does, do not work around it: the string it found is almost certainly a real credential.
Common mistakes
- Committing
.env"temporarily". There is no temporary in Git; the commit is permanent unless history is rewritten (lesson 8.7). - Adding
.envto.gitignoreafter committing it. Ignore rules do not apply to tracked files;git rm --cachedis needed, and the old commits still hold the value. - Putting a token in a URL such as
https://user:token@gitlab.com/…. It lands in.git/configand in shell history. - Sharing values in chat. Use the password manager; chat is searchable and backed up.
- Assuming a private repository is safe enough. It is better than public, but everyone with read access, every fork, every clone and every backup has the value.
Try it yourself
Goal: prove that the ignore rule works before you ever need it to.
- In the playground, create a file with a fake secret:
printf 'API_KEY=sk-live-fake123\n' > .env. - Run
git status --short. It should print nothing. - Run
git check-ignore -v .envand read which rule matched. - Try
git add .envand read the refusal. - Delete the file:
rm .env.
Expected result: step 2 is silent, step 3 names .gitignore:7:.env, and step 4 refuses with the -f hint.
Show solution
The silence in step 2 is the protection working: a file that never appears in git status cannot be committed by git add .. If your playground's .gitignore lacks the .env line, add it now; every real project should have one.