Stop Committing Secrets to Git — A Practical Guide
Secrets in git leak in seconds and live in history forever. Learn 5 habits to keep keys out of commits and mint them into a gitignored .env.local.

Rocco checked the diff before you pushed. He found the key. He was not impressed.

🦝 Rocco: once it's in a commit, it's in the history forever. even after you delete it. surprise.
To stop committing secrets to git, keep real values out of tracked files entirely: put names in a committed .env.example, real keys in a gitignored .env.local, and add a pre-commit scanner. Ringtail Keys reinforces this by minting each scoped key from your .env.example straight into a gitignored .env.local and Infisical, value-free — so the secret never lives anywhere git can see it.
Why is committing secrets to git so dangerous?
Because git never forgets. A secret you commit and then "delete" in a later commit is still sitting in the repository's history, recoverable by anyone who clones it. If the repo is public — or ever becomes public — automated scanners find exposed keys within seconds of the push. The window between "oops" and "abused" is measured in minutes, not days.
Even in a private repo, a committed secret spreads to every clone, every fork, every CI cache, and every backup. Removing it means rewriting history and force-pushing, which breaks everyone else's checkout. Prevention is dramatically cheaper than cleanup — which is why the whole .env.example / .env.local convention exists in the first place.

🦝 Rocco: git is a diary that keeps everything. write your secrets somewhere else.
What should you do if you already committed a secret?
Assume it's compromised and rotate it immediately — a committed key is a leaked key, full stop. Removing it from history (with a tool like git filter-repo) is worth doing, but rotation is what actually protects you, because you can't guarantee no one already cloned or scanned the repo. GitHub's removing sensitive data guide walks through the history rewrite; rotating the key is the part that stops the bleeding. For a rotation that doesn't take prod down, see how to rotate API keys without breaking production.
How do you keep secrets out of git in the first place?
Five habits cover almost every leak:
| Habit | What it prevents |
|---|---|
Commit .env.example, not .env | Names in git, values out — the safe split |
Gitignore .env.local and friends | Real secrets never tracked |
| Add a pre-commit secret scanner | Catches an accidental paste before it's committed |
| Never paste keys into code or comments | No hardcoded fallbacks that ship in a bundle |
| Use per-environment scoped keys | Limits blast radius if one does slip out |
The first two are the foundation: a committed .env.example documents what the project needs, and a gitignored .env.local holds the actual values. Treat .env.example as a manifest and the whole flow gets tighter — see .env.example is a manifest.
How does Ringtail keep secrets out of git?
Ringtail writes real secrets only to files git already ignores. It reads your .env.example, mints each scoped key from the provider's official API, and writes the value to a gitignored .env.local and to Infisical — never to a tracked file, never to a comment, never into your code.
curl -fsSL ringtail.sh | sh
ringtail up
ringtail up starts the local daemon. Your coding agent provisions from .env.example, and because the flow is value-free, the secret values go straight into .env.local — they don't pass through the model, don't land in a transcript, and never touch a tracked file. You approve one "allow" per provider; after that it's zero-touch. The values live exactly where they should, and nowhere git can catch them.
When should you use Ringtail? (and when not)
Use Ringtail when:
- You want new keys minted directly into a gitignored
.env.localinstead of pasted around by hand. - You want a coding agent to provision infra without secrets ever entering a commit or a transcript — see why your agent shouldn't see your API keys.
- You want least-privilege keys so a slip-up has a small blast radius.
- You're setting up a new project and want every key provisioned in one command.
Don't reach for Ringtail when:
- You want a hosted vault that stores existing secrets behind a vendor login — Ringtail acquires keys, it isn't a storage console.
- You need an enterprise compliance and audit platform.
- A provider offers no key-minting API. Ringtail uses official APIs first; where one is missing, it falls back to a guided flow where a human handles the login — never a bot acting with your credentials.
FAQ
What happens if I commit an API key to git?
A committed API key should be treated as compromised, because git history preserves it even after you delete it in a later commit, and automated scanners find keys in public repos within seconds. Rotate the key immediately — that's what actually protects you — and then remove it from history with a tool like git filter-repo. Prevention via a gitignored .env.local is far cheaper than this cleanup.
How do I stop committing secrets to git?
Keep real values out of tracked files: commit a .env.example with names only, put real keys in a gitignored .env.local, and add a pre-commit secret scanner to catch accidental pastes. Never hardcode keys in source or comments. Ringtail Keys reinforces this by minting each scoped key directly into a gitignored .env.local and Infisical, so the value never lives where git can see it.
Is .env.local safe to keep secrets in?
Yes, as long as .env.local is listed in your .gitignore so it's never tracked. The convention is deliberate: .env.example (names, committed) documents the contract, while .env.local (values, ignored) holds the real secrets on your machine only. Ringtail Keys writes to .env.local and Infisical by default, keeping the values out of git.
Does using Ringtail remove secrets already in my history?
No — Ringtail Keys prevents new secrets from entering git, but it doesn't rewrite existing history. If you've already committed a secret, rotate it immediately and use a history-rewriting tool to purge it. Going forward, Ringtail mints keys straight into a gitignored .env.local, value-free, so future secrets never reach a commit in the first place.
