{"id":2581,"date":"2026-05-30T03:31:46","date_gmt":"2026-05-30T03:31:46","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/05\/30\/every-tutorial-tells-you-to-add-env-to-gitignore-thats-not-enough\/"},"modified":"2026-05-30T03:31:46","modified_gmt":"2026-05-30T03:31:46","slug":"every-tutorial-tells-you-to-add-env-to-gitignore-thats-not-enough","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/05\/30\/every-tutorial-tells-you-to-add-env-to-gitignore-thats-not-enough\/","title":{"rendered":"Every tutorial tells you to add .env to .gitignore. That&#8217;s not enough."},"content":{"rendered":"<div>\n<div>\n<div data-article-id=\"3781553\" id=\"article-body\">\n<p>Here&#8217;s something nobody talks about.<\/p>\n<p><code>.gitignore<\/code> doesn&#8217;t encrypt your secrets. It just hides them from git.<\/p>\n<p>They&#8217;re still sitting on your laptop as plaintext. Every tool you install can read them. Every script that runs can read them. One accidental commit and your database password is public on GitHub forever.<\/p>\n<p>So I built <strong>dotlock<\/strong> \u2014 an encrypted <code>.env<\/code> vault with a terminal UI, written in Go.<\/p>\n<h2> <a name=\"\" href=\"#\"> <\/a> <\/h2>\n<h2> <a name=\"before-and-after\" href=\"#before-and-after\"> <\/a> Before and after <\/h2>\n<p><strong>Before dotlock<\/strong> <\/p>\n<div>\n<pre><code><span>DATABASE_URL<\/span><span>=<\/span>postgres:\/\/localhost\/myapp <span># plaintext, readable by anything<\/span> <span>STRIPE_KEY<\/span><span>=<\/span>sk_live_abc123 <span># one grep away from anyone<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><strong>After dotlock<\/strong> <\/p>\n<div>\n<pre><code><span># .dotlock file on disk \u2014 looks like this:<\/span> <span>[<\/span>encrypted binary \u2014 unreadable without your private key] <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<hr>\n<h2> <a name=\"how-it-works-under-10-seconds\" href=\"#how-it-works-under-10-seconds\"> <\/a> How it works under 10 seconds <\/h2>\n<div>\n<pre><code><span>cd <\/span>my-project dotlock <span>set <\/span>DATABASE_URL <span># prompts for value, input is masked<\/span> dotloc <span># opens the terminal UI<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Secrets are encrypted with <a href=\"https:\/\/age-encryption.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">age<\/a> \u2014 X25519 key agreement and ChaCha20-Poly1305 authenticated encryption. The same primitives serious security engineers use. No master password. No cloud. No telemetry. 100% offline.<\/p>\n<hr>\n<h2> <a name=\"what-it-looks-like\" href=\"#what-it-looks-like\"> <\/a> What it looks like <\/h2>\n<p><a href=\"https:\/\/media2.dev.to\/dynamic\/image\/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto\/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1jbkasvnk3lcxymmuizt.png\"><\/a><br \/> Two panels \u2014 profiles on the left, secrets on the right. Values are masked by default. Press <code>v<\/code> to reveal for 30 seconds, then it hides itself automatically.<\/p>\n<p>Switch between <code>dev<\/code>, <code>staging<\/code>, and <code>prod<\/code> profiles. Run a diff before deploying to catch missing variables before they break your app.<\/p>\n<hr>\n<h2> <a name=\"the-interesting-technical-bit\" href=\"#the-interesting-technical-bit\"> <\/a> The interesting technical bit <\/h2>\n<p>The hardest part wasn&#8217;t the encryption \u2014 <a href=\"https:\/\/age-encryption.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">filippo.io\/age<\/a> makes that straightforward.<\/p>\n<p>It was the TUI.<\/p>\n<p><a href=\"https:\/\/github.com\/charmbracelet\/bubbletea\" target=\"_blank\" rel=\"noopener noreferrer\">BubbleTea<\/a> uses the Elm architecture \u2014 Model, Update, View. Everything is a message. A keypress is a message. A timer firing is a message. Your <code>Update<\/code> function receives messages and returns a new model.<\/p>\n<p>The 30-second auto-hide on secret reveal works like this \u2014 no <code>time.Sleep<\/code>, no goroutines: <\/p>\n<div>\n<pre><code><span>type<\/span> <span>secretReveal<\/span> <span>struct<\/span> <span>{<\/span> <span>key<\/span> <span>string<\/span> <span>value<\/span> <span>[]<\/span><span>byte<\/span> <span>expire<\/span> <span>time<\/span><span>.<\/span><span>Time<\/span> <span>\/\/ Now() + 30 seconds<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>On every render, check if <code>time.Now()<\/code> is past the expiry. If it is, zero the bytes and clear the display. Simple once you understand the model but it took me longer than I expected to get right.<\/p>\n<hr>\n<h2> <a name=\"install-it\" href=\"#install-it\"> <\/a> Install it <\/h2>\n<div>\n<pre><code>go <span>install <\/span>github.com\/ahmadraza100\/dotlock@latest <span>cd <\/span>your-project dotlock init dotlock ui <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Or download a binary from the <a href=\"https:\/\/github.com\/ahmadraza100\/dotlock\/releases\/latest\" target=\"_blank\" rel=\"noopener noreferrer\">releases page<\/a> \u2014 Mac, Linux, and Windows all supported.<\/p>\n<p>Full source, architecture docs, and security model on GitHub:<\/p>\n<p>\ud83d\udc49 <strong><a href=\"https:\/\/github.com\/ahmadraza100\/dotlock\" target=\"_blank\" rel=\"noopener noreferrer\">github.com\/ahmadraza100\/dotlock<\/a><\/strong><\/p>\n<hr>\n<p>What do you currently use for managing local secrets? Curious what others are doing \u2014 drop it in the comments.<\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/ahmadraza100\/every-tutorial-tells-you-to-add-env-to-gitignore-thats-not-enough-heres-what-i-built-43c8\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Here&#8217;s something nobody talks about. .gitignore doesn&#8217;t encrypt your secrets. It just hides them from git. They&#8217;re still sitting on your laptop as plaintext. Every tool you install can read them. Every script that runs can read them. One accidental commit and your database password is public on GitHub forever. So I built dotlock \u2014 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2580,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[41],"tags":[],"class_list":["post-2581","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2581","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=2581"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2581\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/2580"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=2581"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=2581"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=2581"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}