PageSourceSearch

https://envless.cloud/assets/how-to-stop-committing-env-files-to-git.CxlYfu6J.js

js envless.cloud collected 2026-10-08 19:59:14 UTC 18,527 bytes, 66 lines download raw bytes

1import{j as e}from"./jsx-runtime.u17CrQMm.js";const a={title:"Stop Committing .env Files to Git",description:"A practical guide to stop committing .env files to Git: gitignore, history cleanup, secret rotation, and encrypted alternatives for managing secrets.",subtitle:"Stop committing .env files: gitignore, history cleanup, secret rotation, and encrypted alternatives.",date:"2026-06-18T00:00:00.000Z",image:"/images/blog/how-to-stop-committing-env-files-to-git.webp",author:"Envless Team",related:["environment-variables-across-environments","what-zero-trust-secrets-management-means"],keywords:["env files","git secrets","gitignore","secrets management","environment variables","secret rotation"]},h=993,n=[{depth:2,slug:"why-a-committed-env-is-worse-than-it-looks",text:"Why a committed .env is worse than it looks"},{depth:2,slug:"step-1-stop-tracking-the-file-going-forward",text:"Step 1: Stop tracking the file going forward"},{depth:2,slug:"step-2-check-whether-the-secret-is-in-history",text:"Step 2: Check whether the secret is in history"},{depth:2,slug:"step-3-purge-it-from-history-if-it-was-committed",text:"Step 3: Purge it from history (if it was committed)"},{depth:2,slug:"step-4-rotate-every-exposed-secret",text:"Step 4: Rotate every exposed secret"},{depth:2,slug:"step-5-add-a-guardrail-so-it-does-not-happen-again",text:"Step 5: Add a guardrail so it does not happen again"},{depth:2,slug:"what-to-do-instead-of-committing-secrets",text:"What to do instead of committing secrets"},{depth:2,slug:"the-short-version",text:"The short version"}];function l(){return n}function t(s){const i={a:"a",blockquote:"blockquote",code:"code",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",span:"span",strong:"strong",ul:"ul",...s.components};return e.jsxs(e.Fragment,{children:[e.jsxs(i.blockquote,{children:[`
2`,e.jsxs(i.p,{children:[e.jsx(i.strong,{children:"TL;DR:"})," Add ",e.jsx(i.code,{children:".env"})," to ",e.jsx(i.code,{children:".gitignore"}),", rotate any secret that ever touched a commit (ignoring it does not remove it from history), and move shared secrets into an encrypted store instead of plaintext files."]}),`
3`]}),`
4`,e.jsxs(i.p,{children:["To stop committing ",e.jsx(i.code,{children:".env"})," files: add them to ",e.jsx(i.code,{children:".gitignore"}),", scrub any that are already tracked, rotate the secrets they contained, and stop passing secrets around as plaintext files. The order matters. Ignoring a file does not delete the secrets already sitting in your Git history, so the cleanup and rotation steps are the ones that actually protect you."]}),`
5`,e.jsx(i.p,{children:"This post walks through each step with copy-pasteable commands, then covers what to use instead of committing secrets at all."}),`
6`,e.jsx(i.h2,{id:"why-a-committed-env-is-worse-than-it-looks",children:"Why a committed .env is worse than it looks"}),`
7`,e.jsxs(i.p,{children:["A ",e.jsx(i.code,{children:".env"})," file in a commit is not just visible to your team. It is permanently recorded in history, included in every clone, and pushed to every fork and mirror. If the repo is ever made public, or a laptop with a clone is compromised, every key in that file is exposed."]}),`
8`,e.jsx(i.p,{children:"Two properties make this especially dangerous:"}),`
9`,e.jsxs(i.ul,{children:[`
10`,e.jsxs(i.li,{children:[e.jsx(i.strong,{children:"History is sticky."})," Deleting the file in a new commit leaves the old version reachable through ",e.jsx(i.code,{children:"git log"}),", tags, and branches."]}),`
11`,e.jsxs(i.li,{children:[e.jsx(i.strong,{children:"Bots are fast."})," Public repositories are scanned continuously. Leaked cloud keys are often used within minutes of being pushed."]}),`
12`]}),`
13`,e.jsx(i.p,{children:"The practical conclusion: once a secret has been committed and pushed, treat it as compromised. Cleanup limits the blast radius, but rotation is what makes the key safe again."}),`
14`,e.jsx(i.h2,{id:"step-1-stop-tracking-the-file-going-forward",children:"Step 1: Stop tracking the file going forward"}),`
15`,e.jsxs(i.p,{children:["Add the file to ",e.jsx(i.code,{children:".gitignore"})," so Git ignores future changes:"]}),`
16`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"plaintext","data-lang":"plaintext",children:e.jsxs(i.code,{children:[e.jsx(i.span,{className:"line",children:e.jsx(i.span,{children:"# .gitignore"})}),`
17`,e.jsx(i.span,{className:"line",children:e.jsx(i.span,{children:".env"})}),`
18`,e.jsx(i.span,{className:"line",children:e.jsx(i.span,{children:".env.*"})}),`
19`,e.jsx(i.span,{className:"line",children:e.jsx(i.span,{children:"!.env.example"})})]})}),`
20`,e.jsxs(i.p,{children:["The ",e.jsx(i.code,{children:"!.env.example"})," line keeps a checked-in template (with empty or dummy values) so teammates know which variables they need without seeing real secrets. You can generate that template automatically from an existing file with the ",e.jsx(i.a,{href:"/tools",children:"env-example generator"}),"."]}),`
21`,e.jsxs(i.p,{children:["If the file is already tracked, ",e.jsx(i.code,{children:".gitignore"})," alone does nothing. Untrack it without deleting your local copy:"]}),`
22`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsxs(i.code,{children:[e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" rm"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --cached"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" .env"})]}),`
23`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" commit"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" -m"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:' "Stop tracking .env"'})]})]})}),`
24`,e.jsx(i.h2,{id:"step-2-check-whether-the-secret-is-in-history",children:"Step 2: Check whether the secret is in history"}),`
25`,e.jsx(i.p,{children:"Before you celebrate, find out if the file was ever committed:"}),`
26`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsx(i.code,{children:e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" log"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --all"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --full-history"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" .env"})]})})}),`
27`,e.jsx(i.p,{children:"If that prints commits, the secrets in those versions are still recoverable by anyone with the repo. Move to Step 3 and Step 4."}),`
28`,e.jsx(i.h2,{id:"step-3-purge-it-from-history-if-it-was-committed",children:"Step 3: Purge it from history (if it was committed)"}),`
29`,e.jsxs(i.p,{children:["Rewriting history is disruptive. It changes commit hashes and requires everyone to re-clone or hard-reset, so coordinate with your team first. The cleanest tool is ",e.jsx(i.a,{href:"https://github.com/newren/git-filter-repo",children:e.jsx(i.code,{children:"git-filter-repo"})}),":"]}),`
30`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsxs(i.code,{children:[e.jsx(i.span,{className:"line",children:e.jsx(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#8B949E"},children:"# Remove the file from every commit"})}),`
31`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" filter-repo"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --path"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" .env"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --invert-paths"})]})]})}),`
32`,e.jsx(i.p,{children:"Then force-push the rewritten history:"}),`
33`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsxs(i.code,{children:[e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" push"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" origin"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --force"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --all"})]}),`
34`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" push"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" origin"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --force"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --tags"})]})]})}),`
35`,e.jsx(i.p,{children:"Important caveats:"}),`
36`,e.jsxs(i.ul,{children:[`
37`,e.jsx(i.li,{children:"Force-pushing does not retract data already cloned, cached by your Git host, or sitting in open pull requests. Some hosts retain unreachable commits for a while."}),`
38`,e.jsxs(i.li,{children:["This is why purging is necessary but ",e.jsx(i.strong,{children:"not sufficient"}),". The next step is the one that actually closes the hole."]}),`
39`]}),`
40`,e.jsx(i.h2,{id:"step-4-rotate-every-exposed-secret",children:"Step 4: Rotate every exposed secret"}),`
41`,e.jsxs(i.p,{children:["Assume every value that ever lived in a committed ",e.jsx(i.code,{children:".env"})," is public, and rotate it: database passwords, API keys, OAuth client secrets, signing keys, webhook tokens. Generate fresh, high-entropy replacements, then update them in each provider and in your deployment environment. A ",e.jsx(i.a,{href:"/tools",children:"secret generator"})," is handy for the values you create yourself."]}),`
42`,e.jsx(i.p,{children:"Rotation is the only step that is guaranteed to work regardless of who already pulled the old history. Do it even if you are confident the repo was always private."}),`
43`,e.jsx(i.h2,{id:"step-5-add-a-guardrail-so-it-does-not-happen-again",children:"Step 5: Add a guardrail so it does not happen again"}),`
44`,e.jsx(i.p,{children:"Manual discipline fails eventually. Add a pre-commit hook that blocks secrets before they land:"}),`
45`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsxs(i.code,{children:[e.jsx(i.span,{className:"line",children:e.jsx(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#8B949E"},children:"# .git/hooks/pre-commit (or via the pre-commit framework)"})}),`
46`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"if"}),e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:" git"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" diff"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --cached"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --name-only"}),e.jsx(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:" |"}),e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:" grep"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" -qE"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" '(^|/)\\.env($|\\.)'"}),e.jsx(i.span,{style:{"--shiki-light":"#24292E","--shiki-dark":"#E6EDF3"},children:"; "}),e.jsx(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"then"})]}),`
47`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"  echo"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:' "Blocked: attempt to commit a .env file"'})]}),`
48`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:"  exit"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" 1"})]}),`
49`,e.jsx(i.span,{className:"line",children:e.jsx(i.span,{style:{"--shiki-light":"#D73A49","--shiki-dark":"#FF7B72"},children:"fi"})})]})}),`
50`,e.jsxs(i.p,{children:["Tools like ",e.jsx(i.code,{children:"gitleaks"})," or ",e.jsx(i.code,{children:"trufflehog"})," go further and scan staged diffs for high-entropy strings and known key formats. Run them in CI too, so a bypassed local hook still gets caught."]}),`
51`,e.jsx(i.h2,{id:"what-to-do-instead-of-committing-secrets",children:"What to do instead of committing secrets"}),`
52`,e.jsxs(i.p,{children:[e.jsx(i.code,{children:".gitignore"}
52)," keeps secrets out of the repo, but it does not solve the real problem: your team still needs to share and sync those values somehow. Passing a ",e.jsx(i.code,{children:".env"})," around over Slack or a shared drive just moves the plaintext to a different insecure place."]}),`
53`,e.jsx(i.p,{children:"The better pattern is to keep secrets out of files entirely and inject them at runtime from an encrypted store:"}),`
54`,e.jsx(i.pre,{className:"shiki-code shiki-code-themes github-light github-dark-default",style:{"--shiki-light":"#24292e","--shiki-dark":"#e6edf3","--shiki-light-bg":"#fff","--shiki-dark-bg":"#0d1117",overflowX:"auto",whiteSpace:"pre-wrap",wordWrap:"break-word"},tabIndex:"0","data-language":"bash","data-lang":"bash",children:e.jsxs(i.code,{children:[e.jsx(i.span,{className:"line",children:e.jsx(i.span,{style:{"--shiki-light":"#6A737D","--shiki-dark":"#8B949E"},children:"# Pull the latest secrets for this environment, then run the app"})}),`
55`,e.jsxs(i.span,{className:"line",children:[e.jsx(i.span,{style:{"--shiki-light":"#6F42C1","--shiki-dark":"#FFA657"},children:"envless"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" run"}),e.jsx(i.span,{style:{"--shiki-light":"#005CC5","--shiki-dark":"#79C0FF"},children:" --"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" npm"}),e.jsx(i.span,{style:{"--shiki-light":"#032F62","--shiki-dark":"#A5D6FF"},children:" start"})]})]})}),`
56`,e.jsxs(i.p,{children:["This is the model ",e.jsx(i.a,{href:"/",children:"Envless"})," uses. Variable values are encrypted client-side on your device before they ever reach the server, so the backend stores only ciphertext and never sees plaintext. Secrets sync across local, staging, production, CI, and your whole team, with versioning, transactional publish and rollback, and an attributed change history on every variable. Sign-in is passwordless, so there is no reusable password to leak, and account access on its own reveals no value without the workspace passphrase. There are typed SDKs for TypeScript, Python and Ruby, plus a REST API for every other language, when you need to read or write values from code."]}),`
57`,e.jsxs(i.p,{children:["The free tier covers one project with no credit card, which is enough to migrate a single repo off committed ",e.jsx(i.code,{children:".env"})," files and see how it feels; the ",e.jsx(i.a,{href:"/pricing",children:"pricing page"})," breaks down team plans if you outgrow it."]}),`
58`,e.jsx(i.h2,{id:"the-short-version",children:"The short version"}),`
59`,e.jsxs(i.ol,{children:[`
60`,e.jsxs(i.li,{children:["Add ",e.jsx(i.code,{children:".env"})," to ",e.jsx(i.code,{children:".gitignore"})," and untrack it."]}),`
61`,e.jsxs(i.li,{children:["Check history; purge with ",e.jsx(i.code,{children:"git-filter-repo"})," if it was ever committed."]}),`
62`,e.jsx(i.li,{children:"Rotate every exposed secret. This is non-negotiable."}),`
63`,e.jsx(i.li,{children:"Add a pre-commit hook plus CI secret scanning."}),`
64`,e.jsx(i.li,{children:"Replace plaintext file sharing with an encrypted, synced secrets store."}),`
65`]}),`
66`,e.jsxs(i.p,{children:["For more on why ",e.jsx(i.code,{children:".env"})," files struggle at team scale, see the rest of the ",e.jsx(i.a,{href:"/blog",children:"blog"}),"."]})]})}function o(s={}){const{wrapper:i}=s.components||{};return i?e.jsx(i,{...s,children:e.jsx(t,{...s})}):t(s)}export{o as default,a as frontmatter,l as getHeadings,n as headings,h as wordCount};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.