<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="https://dotenvx.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dotenvx.com/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-10-09T02:16:47+00:00</updated><id>https://dotenvx.com/feed.xml</id><title type="html">Dotenvx</title><subtitle>Encrypt your secrets. Ship them with your code.</subtitle><author><name>Dotenvx</name></author><entry><title type="html">Faster than Node&apos;s native parseEnv?</title><link href="https://dotenvx.com/blog/2026/09/28/dotenv-fast.html" rel="alternate" type="text/html" title="Faster than Node&apos;s native parseEnv?" /><published>2026-09-28T00:00:00+00:00</published><updated>2026-09-28T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2026/09/28/dotenv-fast</id><content type="html" xml:base="https://dotenvx.com/blog/2026/09/28/dotenv-fast.html"><![CDATA[<p>Faster than Node's native parseEnv?</p>

<p>No, way.</p>

<p>Yes. Yes, way.</p>

<p><a href="https://github.com/motdotla/dotenv">Dotenv's</a> fast mode now beats Node's native parser on all current LTS versions - Node 22 and 24 - across Linux and Windows in our benchmarks. See below where Node native is still faster.</p>

<h2 id="background">Background</h2>

<p>Five months ago, the wildest PR ever landed in dotenv. Dotenv contributions are typically small parser changes, type detail additions, or proposals for all new features. This was something different.</p>

<p>This was a new parser, claiming 2x speedup.</p>

<p><img src="https://github.com/user-attachments/assets/572df7e0-a186-4beb-9aeb-93042cad0649" alt="Pull request introducing the faster dotenv parser" loading="lazy" /></p>

<p>Some guy from Sweden, <a href="https://github.com/homanp">homanp</a>. A character scanning approach. Pretty sick, tbh.</p>

<p>So what did I do? What any maintainer does for a package depended on by <a href="https://github.com/motdotla/dotenv/network/dependents">tens of millions</a>. I sat on it. lol.</p>

<p>But we also got in touch with each other and had a chat. He and his cofounder ran a service called <a href="https://superagent.sh">Superagent.sh</a> - securing PRs. They generously offered to secure Dotenvx's, and today they do that for all kinds of open source projects - even <a href="https://github.com/firecrawl/firecrawl">Firecrawl</a>.</p>

<h2 id="the-release">The Release</h2>

<p>Back to Dotenv. The parser was contributed as a full replacement. That's too risky so we put it behind the --fast flag. On September 17th, five months after that initial PR dropped, we released it.</p>

<p><img src="https://github.com/user-attachments/assets/d1792485-6e04-4f05-8a34-4f34f98e2e04" alt="Dotenv changelog announcing the fast parser" loading="lazy" /></p>

<p>It's fast.</p>

<div class="design-card">
  


<div class="design-table-wrap design-table-wrap--fill">
  <table class="design-table">
    
    
<thead>
  <tr><th scope="col">Node</th><th scope="col">Linux</th><th scope="col">Windows</th></tr>
</thead>
<tbody>
  <tr><th scope="row" style="white-space: nowrap;">20</th><td style="white-space: nowrap;">Native wins</td><td>Native wins</td></tr>
  <tr><th scope="row" style="white-space: nowrap;">22 LTS</th><td style="white-space: nowrap;"><strong>Dotenv wins</strong></td><td><strong>Dotenv wins</strong></td></tr>
  <tr><th scope="row" style="white-space: nowrap;">24 LTS</th><td style="white-space: nowrap;"><strong>Dotenv wins</strong></td><td><strong>Dotenv wins</strong></td></tr>
  <tr><th scope="row" style="white-space: nowrap;">26 Current</th><td style="white-space: nowrap;">Native wins</td><td><strong>Dotenv wins Buffers</strong>; strings tied</td></tr>
</tbody>

  </table>
</div>


</div>

<p style="margin-top: .375rem; font-size: var(--design-text-micro); color: var(--design-mid);">Source: <a style="font-size: inherit; color: inherit !important;" href="https://github.com/motdotla/dotenv/actions/runs/35872614990">GitHub Actions benchmarks, September 23, 2026</a>.</p>

<p>Since then the community is using it, have found edge cases, and those have been patched. It's cool to see, and I won't be surprised if it becomes adopted by other dotenv implementations.</p>

<h2 id="usage">Usage</h2>

<p>With dotenv 18 or later, enable fast mode in your app:</p>

<div class="design-codeblock-wrap" data-design-codeblock="">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">dotenv</span><span class="dl">'</span><span class="p">).</span><span class="nf">config</span><span class="p">({</span> <span class="na">fast</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span></code></pre>
</div>

<p>Or use the new CLI:</p>

<div class="design-codeblock-wrap" data-design-codeblock="" data-copy="npx dotenv run --fast -- node index.js">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="gp">$ </span>npx dotenv run <span class="nt">--fast</span> <span class="nt">--</span> node index.js</code></pre>
</div>

<p>Dotenv fast mode in action.</p>

<figure data-design-video="" class="design-video">
  <div class="design-video-frame">
    <video class="design-video-player" controls="" preload="metadata" playsinline="">
      
        <source src="https://github.com/user-attachments/assets/8f69bf7a-e7f1-49b9-8650-c0dcb37809e6" type="video/mp4" />
      
      
      
        Download the <a class="design-link" href="https://github.com/user-attachments/assets/8f69bf7a-e7f1-49b9-8650-c0dcb37809e6">mp4</a> video.
      
    </video>
    <button class="design-video-preview-play design-video-play" type="button" aria-label="Play video" hidden="">
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v13.72L19.5 12 8 5.14z" /></svg>
    </button>
  </div>
  
</figure>

<h2 id="the-future">The Future</h2>

<p>Software is changing, fast. With Dotenv we're committed to maintaining the past while still evolving .env. This --fast parser is an example of that.</p>

<p>If you love .env and also want to see it evolve, please support us. Use <a href="https://github.com/motdotla/dotenv">dotenv</a>, use <a href="https://github.com/dotenvx/dotenvx">dotenvx</a>, and even snag a <a href="https://dotenvx.com/membership">dotenv membership</a>. Be part of the .env story. It's not all written yet.</p>

<div class="blog-partner-thanks">
  <p>Lastly, a giant thank you to <a href="https://superagent.sh">Superagent.sh</a> for bringing this to the dotenv community.</p>
  <a class="blog-partner-logo" href="https://superagent.sh" aria-label="Visit Superagent.sh">
    <img class="blog-partner-logo-light" src="https://www.superagent.sh/images/superagent-logo-square-flat-favicon.webp" alt="Superagent.sh" width="48" height="48" loading="lazy" />
    <img class="blog-partner-logo-dark" src="https://www.superagent.sh/images/superagent-logo-square-flat-favicon-dark.webp" alt="Superagent.sh" width="48" height="48" loading="lazy" />
    <span>superagent_</span>
  </a>
</div>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[Dotenv's fast mode now beats Node's native parser on all current LTS versions - Node 22 and 24 - across Linux and Windows in our benchmarks.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2026-09-28-dotenv-fast-html-1255358f1dd42251.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2026-09-28-dotenv-fast-html-1255358f1dd42251.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Dotenv VS Code: 1.0 and beyond</title><link href="https://dotenvx.com/blog/2026/09/21/dotenv-vscode.html" rel="alternate" type="text/html" title="Dotenv VS Code: 1.0 and beyond" /><published>2026-09-21T00:00:00+00:00</published><updated>2026-09-21T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2026/09/21/dotenv-vscode</id><content type="html" xml:base="https://dotenvx.com/blog/2026/09/21/dotenv-vscode.html"><![CDATA[<p>We're back to work on <a href="https://marketplace.visualstudio.com/items?itemName=dotenv.dotenv-vscode">Dotenv Official</a>, our VS Code extension. After a long stretch on 0.x, we've graduated it to <strong>1.0</strong> and kept going.</p>

<p>A lot is changing about how we write code. Agents do more of the typing. But editing a .env file is still something many of us do ourselves: paste in an API key, change a database URL, or add a variable for a new service.</p>

<p>That puts secrets right on your screen. Open the wrong tab during a screen share and they're there for everyone to see.</p>

<p>The extension's job is simple: <strong>keep secrets out of sight while you work in your editor.</strong> Reveal them when you need them, then get back to work. That's the focus of 1.0 and the releases that followed.</p>

<h2 id="open-your-env-without-the-flash">Open your .env without the flash</h2>

<p>Auto-cloaking has been part of the extension for years. But a limitation of VS Code's native editor meant secrets could briefly flash before the extension could mask them when opening a file or switching tabs.</p>

<p>For 1.0, we worked around that limitation by building our own Dotenv Editor. It masks values before the editor becomes visible. Open your .env and the names are readable, while the values stay hidden.</p>

<p><img src="https://github.com/user-attachments/assets/53d25529-ab04-4c5d-8a74-ae6cb39b93cf" alt="Opening a dotenv file with values cloaked and toggling their visibility" loading="lazy" /></p>

<p>Click <strong>Toggle auto-cloaking</strong> to reveal or hide values. Switching tabs hides them again. Editing, find and replace, multiple cursors, undo, and redo still work. Cloaking only changes what you see; copying a value copies its actual text.</p>

<p>We've also added cloaking for Docker Compose environment values. YAML uses the native editor, so it doesn't have the same no-flash behavior as the Dotenv Editor.</p>

<h2 id="complete-names-reveal-when-needed">Complete names. Reveal when needed.</h2>

<p>Start typing process.env. and choose a variable from your dotenv files. Suggestions show the source filenames and keep values masked.</p>

<p><img src="https://github.com/user-attachments/assets/ad2b730e-d32f-4573-acd8-f7491bf48e25" alt="Autocompleting an environment variable with its source file shown" loading="lazy" /></p>

<p>Already have a variable in your code? Hover over it to see its source, then use <strong>Reveal value</strong> or <strong>Hide value</strong> in the popup.</p>

<p><img src="https://github.com/user-attachments/assets/f4441d8d-8765-4849-b947-6ce36412ca03" alt="Peeking at an environment variable from source code using the reveal control" loading="lazy" /></p>

<p>The latest releases add support for <code class="language-plaintext highlighter-rouge">import { env } from 'node:process'</code>, including aliases, plus Julia, Erlang, Swift, and Clojure. Nested dotenv files are discovered too, with lookups scoped to the source file's directory and its parents up to the workspace root.</p>

<h2 id="still-your-env-file">Still your .env file</h2>

<p>You can keep working with the .env files you already have. Syntax highlighting keeps names, values, comments, and quoted strings easy to read. The new editor also highlights numeric values.</p>

<p><img src="https://github.com/user-attachments/assets/f276e078-c60e-4cb2-9664-f6750d8387c9" alt="Syntax highlighting for dotenv variable names, values, and comments" loading="lazy" /></p>

<h2 id="encrypted-files-too">Encrypted files, too</h2>

<p>If you use dotenvx encryption, the latest releases also let you hover over an encrypted: value and click <strong>Decrypt value</strong>. The file stays encrypted, and the displayed plaintext clears when you hide it, leave the popup, or switch tabs.</p>

<p><img src="https://github.com/user-attachments/assets/ba5ec15b-1be8-40b1-902d-116fdbf81591" alt="Decrypting an encrypted value in the Dotenv Editor without changing the file" loading="lazy" /></p>

<p>This feature needs the <a href="/install">dotenvx CLI</a> installed locally, a saved file in a trusted workspace, and the matching private key available locally. You don't need encryption or the CLI to use the extension's cloaking, completion, and syntax highlighting.</p>

<h2 id="try-it">Try it</h2>

<p><a href="https://marketplace.visualstudio.com/items?itemName=dotenv.dotenv-vscode">Install or update Dotenv Official</a> to <strong>1.5.2 or later</strong>. You can find the full release history in the <a href="https://github.com/dotenvx/dotenv-vscode/blob/master/CHANGELOG.md">changelog</a>.</p>

<p>As more of our coding moves to agents, the moments we spend editing secrets ourselves still deserve care. Dotenv for VS Code is back, past 1.0, and focused on making that everyday task a little safer.</p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[The official dotenv extension is back, with a 1.0 release focused on keeping secrets out of sight while you edit your .env files.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2026-09-21-dotenv-vscode-html-48e83d74d13ecdd9.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2026-09-21-dotenv-vscode-html-48e83d74d13ecdd9.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Dotenv 18: CLI</title><link href="https://dotenvx.com/blog/2026/09/19/dotenv-18.html" rel="alternate" type="text/html" title="Dotenv 18: CLI" /><published>2026-09-19T00:00:00+00:00</published><updated>2026-09-19T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2026/09/19/dotenv-18</id><content type="html" xml:base="https://dotenvx.com/blog/2026/09/19/dotenv-18.html"><![CDATA[<p>It's the biggest change to Dotenv in a decade, a CLI.</p>

<p>For years, dotenv has loaded your .env file from inside your application. Now it can load those variables before your command starts. Your app just reads its environment.</p>

<p>That makes the same .env file useful for scripts, tests, and other command-line tools, too. And it gives coding agents a simple way to run a command with the environment it needs.</p>

<figure data-design-video="" class="design-video">
  <div class="design-video-frame">
    <video class="design-video-player" controls="" preload="metadata" playsinline="">
      
        <source src="https://github.com/user-attachments/assets/79395e92-5ce0-430d-b8b5-31631aac25bb" type="video/mp4" />
      
      
      
        Download the <a class="design-link" href="https://github.com/user-attachments/assets/79395e92-5ce0-430d-b8b5-31631aac25bb">mp4</a> video.
      
    </video>
    <button class="design-video-preview-play design-video-play" type="button" aria-label="Play Dotenv CLI in action" hidden="">
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v13.72L19.5 12 8 5.14z" /></svg>
    </button>
  </div>
  <figcaption class="design-video-title">Dotenv CLI in action</figcaption>
</figure>

<h2 id="usage">Usage</h2>

<p>Install <a href="https://github.com/motdotla/dotenv">dotenv@18.0.0</a> or greater.</p>

<div class="design-codeblock-wrap" data-design-codeblock="" data-copy="npm install dotenv --save">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="gp">$ </span>npm <span class="nb">install </span>dotenv <span class="nt">--save</span></code></pre>
</div>

<p>Create your .env file.</p>

<div class="design-codeblock-wrap" data-design-codeblock="">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c"># .env</span>
<span class="nv">HELLO</span><span class="o">=</span><span class="s2">"World"</span></code></pre>
</div>

<p>Create a simple app. There's no dotenv import needed here.</p>

<div class="design-codeblock-wrap" data-design-codeblock="">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c1">// index.js</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Hello </span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">HELLO</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span></code></pre>
</div>

<p>Run it with the new CLI.</p>

<div class="design-codeblock-wrap" data-design-codeblock="" data-copy="npx dotenv run -- node index.js">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="gp">$ </span>npx dotenv run <span class="nt">--</span> node index.js
◇ injected env (1) from .env
Hello World</code></pre>
</div>

<p>That's it. Dotenv loads .env, then starts Node with those variables available. The command after the -- can be any executable available on your PATH.</p>

<h2 id="choose-your-environment">Choose your environment</h2>

<p>By default, the CLI reads .env. To use another file, pass -f:</p>

<div class="design-codeblock-wrap" data-design-codeblock="" data-copy="npx dotenv run -f .env.local -- node index.js">
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy="" aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg><span class="design-copy-status" data-design-codeblock-label="" aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="gp">$ </span>npx dotenv run <span class="nt">-f</span> .env.local <span class="nt">--</span> node index.js</code></pre>
</div>

<p>Variables already set in your environment take precedence. Add --override if you want the file's values to win instead.</p>

<h2 id="upgrading">Upgrading</h2>

<p>The familiar <code class="language-plaintext highlighter-rouge">require('dotenv').config()</code> API is still here. If you used Node's -r dotenv/config preload, switch to the CLI command above. Version 18 also removes the legacy .env.vault format; use <a href="/">dotenvx</a> for encrypted .env files.</p>

<p>There's an optional faster parser, too: add --fast to your CLI command to try it. See the <a href="https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md#1800-2026-09-17">release notes</a> for the full changes.</p>

<h2 id="a-small-command-a-big-step">A small command, a big step</h2>

<p>Dotenv started with a simple idea: load your .env file and get to work. The CLI brings that same simplicity to any command - whether you run it yourself or an agent runs it for you. A small command, and a big next step for dotenv.</p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[The biggest change to dotenv in a decade – a CLI.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2026-09-19-dotenv-18-html-44e7c68533187ae5.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2026-09-19-dotenv-18-html-44e7c68533187ae5.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Harden .env.local: dotenvx + OS Keychain</title><link href="https://dotenvx.com/blog/2026/04/02/dotenvx-keychain.html" rel="alternate" type="text/html" title="Harden .env.local: dotenvx + OS Keychain" /><published>2026-04-02T00:00:00+00:00</published><updated>2026-04-02T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2026/04/02/dotenvx-keychain</id><content type="html" xml:base="https://dotenvx.com/blog/2026/04/02/dotenvx-keychain.html"><![CDATA[<p><em>Guest Post by <a href="https://ustunozgur.com">Ustun Ozgur</a></em></p>

<blockquote>
  <p><em>This post originally appeared on <a href="https://dev.to/ustun/a-small-hardening-trick-for-envlocal-dotenvx-os-keychain-2533">dev.to</a>.</em></p>
</blockquote>

<p>Most teams keep local secrets in <code class="language-plaintext highlighter-rouge">.env.local</code> and add that file to <code class="language-plaintext highlighter-rouge">.gitignore</code>.
That is the bare minimum, and it does not address a more pressing risk: supply
chain attacks and compromised local tooling that read <code class="language-plaintext highlighter-rouge">.env</code> files as soon as
they get repo access.</p>

<p>Once a malicious dependency, postinstall script, editor extension, MCP server,
AI coding tool, or other local helper can inspect your workspace, plain-text
<code class="language-plaintext highlighter-rouge">.env.local</code> files become low-effort, high-value targets.</p>

<p>I wanted a low-friction way to reduce that blast radius without forcing the whole
team onto a heavyweight secrets manager for day-to-day local development.</p>

<p>This is the pattern I landed on:</p>

<ul>
  <li>keep non-secret local config in <code class="language-plaintext highlighter-rouge">.env.local</code></li>
  <li>move actual secrets into <code class="language-plaintext highlighter-rouge">.env.local.secrets</code></li>
  <li>encrypt <code class="language-plaintext highlighter-rouge">.env.local.secrets</code> with <code class="language-plaintext highlighter-rouge">dotenvx</code></li>
  <li>move the decryption key out of disk and into macOS Keychain</li>
  <li>load <code class="language-plaintext highlighter-rouge">.env.local</code> first, then only decrypt secrets when an explicit opt-in
flag says to</li>
</ul>

<p>Important distinction: I am <strong>not</strong> using <code class="language-plaintext highlighter-rouge">dotenvx</code> the way it is often
marketed, where encrypted env files are committed to the repo and shared that
way. This setup is local-only. The encrypted file and <code class="language-plaintext highlighter-rouge">.env.keys</code> both stay
uncommitted, and I prefer it that way. Committing encrypted env files is useful
when you want team-wide encrypted config distribution, but that was not my goal.
I wanted to reduce plaintext secrets on developer machines and raise the cost of
tools that slurp local env files, while keeping the workflow simple enough that
teammates actually use it.</p>

<h2 id="the-setup">The setup</h2>

<p>Start with a normal <code class="language-plaintext highlighter-rouge">.env.local</code>, then split it:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">.env.local</code>: safe local config, feature flags, non-secret defaults</li>
  <li><code class="language-plaintext highlighter-rouge">.env.local.secrets</code>: secrets only</li>
</ul>

<p>Example:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c"># .env.local</span>
<span class="nv">BETTER_AUTH_URL</span><span class="o">=</span><span class="sx">http://localhost:3000</span>
<span class="nv">USE_KEYCHAIN_FOR_DOTX</span><span class="o">=</span><span class="sx">true</span></code></pre>
</div>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c"># .env.local.secrets</span>
<span class="nv">POSTGRES_URL</span><span class="o">=</span><span class="sx">postgres://...</span>
<span class="nv">GOOGLE_CLIENT_SECRET</span><span class="o">=</span><span class="sx">...</span>
<span class="nv">BETTER_AUTH_SECRET</span><span class="o">=</span><span class="sx">...</span></code></pre>
</div>

<p>Make sure your <code class="language-plaintext highlighter-rouge">.gitignore</code> covers all the pieces:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>.env.local
.env.local.secrets
.env.keys</code></pre>
</div>

<p>Then encrypt the secrets file:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>pnpm <span class="nb">exec </span>dotenvx encrypt <span class="nt">-f</span> .env.local.secrets</code></pre>
</div>

<p>That gives you an encrypted <code class="language-plaintext highlighter-rouge">.env.local.secrets</code> and a decryption key in
<code class="language-plaintext highlighter-rouge">.env.keys</code>.</p>

<p>At this point, you have improved at-rest security a bit, but the key is still on
disk, which is not the end state we want.</p>

<h2 id="why-bother-with-the-extra-steps">Why bother with the extra steps?</h2>

<p>There have been enough supply chain and developer tooling incidents lately that I
no longer treat "it is gitignored" as a meaningful security boundary. Once
something malicious lands in your development environment, one of the first
profitable things it can do is read local env files and exfiltrate credentials.</p>

<p>Encrypting local secrets at rest is not a complete defense, but it is a useful
speed bump:</p>

<ul>
  <li>secrets are no longer sitting in plaintext on disk</li>
  <li>the decryption key can live in the OS keychain instead of another dotfile</li>
  <li>accidental repo-wide file reads become less damaging</li>
  <li>you can keep the workflow mostly compatible with existing frameworks</li>
</ul>

<p>This does <strong>not</strong> protect secrets after your app starts. At runtime, the process
still has decrypted environment variables in memory. But that is still better
than leaving everything plaintext on disk all the time.</p>

<h2 id="move-the-key-into-macos-keychain">Move the key into macOS Keychain</h2>

<p>Copy the <code class="language-plaintext highlighter-rouge">DOTENV_PRIVATE_KEY_LOCAL_SECRETS</code> value from <code class="language-plaintext highlighter-rouge">.env.keys</code>, then store it
in Keychain:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>security add-generic-password <span class="nt">-U</span> <span class="se">\</span>
  <span class="nt">-a</span> LOCAL_SECRETS_DOTENVX_KEY <span class="se">\</span>
  <span class="nt">-s</span> LOCAL_SECRETS_DOTENVX_KEY <span class="se">\</span>
  <span class="nt">-w</span></code></pre>
</div>

<p>With <code class="language-plaintext highlighter-rouge">security</code>, keeping <code class="language-plaintext highlighter-rouge">-w</code> as the last argument makes it prompt you for the
secret instead of putting it in your shell history.</p>

<p>The <code class="language-plaintext highlighter-rouge">LOCAL_SECRETS_DOTENVX_KEY</code> label is just an example. Pick any consistent
Keychain item name you want, then use that same name everywhere in your scripts.</p>

<p>Now you can delete <code class="language-plaintext highlighter-rouge">.env.keys</code>. Before you do, stash the key somewhere safe
outside the repo. A password manager like 1Password is a good choice. You will
need it if you set up another machine or need to recover.</p>

<p>With that, your decryption key is no longer sitting next to the repo in another
plaintext file.</p>

<blockquote>
  <p><strong>Linux and Windows.</strong> This post uses macOS Keychain, but the same idea
applies elsewhere. On Linux, <code class="language-plaintext highlighter-rouge">secret-tool</code> (backed by <code class="language-plaintext highlighter-rouge">libsecret</code> and
GNOME Keyring or KWallet) fills the same role. On Windows, you can use
Credential Manager via PowerShell's <code class="language-plaintext highlighter-rouge">Get-StoredCredential</code> /
<code class="language-plaintext highlighter-rouge">New-StoredCredential</code> cmdlets. The loading pattern stays the same; only
the key retrieval command changes.</p>
</blockquote>

<h2 id="the-loading-pattern">The loading pattern</h2>

<p>The subtle part is loader order.</p>

<p>If you want a flag like <code class="language-plaintext highlighter-rouge">USE_KEYCHAIN_FOR_DOTX=true</code> to live in <code class="language-plaintext highlighter-rouge">.env.local</code>,
your app needs to read <code class="language-plaintext highlighter-rouge">.env.local</code> <strong>before</strong> it decides whether to pull the
decryption key from Keychain.</p>

<p>That means the loader should do this:</p>

<ol>
  <li>Load <code class="language-plaintext highlighter-rouge">.env</code></li>
  <li>Load <code class="language-plaintext highlighter-rouge">.env.local</code></li>
  <li>Check <code class="language-plaintext highlighter-rouge">USE_KEYCHAIN_FOR_DOTX</code></li>
  <li>If enabled, read <code class="language-plaintext highlighter-rouge">DOTENV_PRIVATE_KEY_LOCAL_SECRETS</code> from Keychain</li>
  <li>Load <code class="language-plaintext highlighter-rouge">.env.local.secrets</code></li>
</ol>

<p>Here is the core idea in TypeScript:</p>

<p><a href="https://gist.github.com/ustun/1f5a9974394cc32bba066e5584243ada">Open as GitHub Gist</a></p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">execFileSync</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:child_process</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">existsSync</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:fs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">resolve</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:path</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">config</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@dotenvx/dotenvx</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">loadEnv</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
  <span class="nf">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">file</span> <span class="k">of</span> <span class="p">[</span><span class="dl">"</span><span class="s2">.env</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">.env.local</span><span class="dl">"</span><span class="p">])</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nf">resolve</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nf">cwd</span><span class="p">(),</span> <span class="nx">file</span><span class="p">);</span>
    <span class="nf">if </span><span class="p">(</span><span class="nf">existsSync</span><span class="p">(</span><span class="nx">path</span><span class="p">))</span> <span class="p">{</span>
      <span class="nf">config</span><span class="p">({</span> <span class="nx">path</span> <span class="p">});</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">localSecretsPath</span> <span class="o">=</span> <span class="nf">resolve</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nf">cwd</span><span class="p">(),</span> <span class="dl">"</span><span class="s2">.env.local.secrets</span><span class="dl">"</span><span class="p">);</span>
  <span class="nf">if </span><span class="p">(</span><span class="o">!</span><span class="nf">existsSync</span><span class="p">(</span><span class="nx">localSecretsPath</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nf">if </span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">USE_KEYCHAIN_FOR_DOTX</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">true</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DOTENV_PRIVATE_KEY_LOCAL_SECRETS</span> <span class="o">=</span> <span class="nf">execFileSync</span><span class="p">(</span>
        <span class="dl">"</span><span class="s2">security</span><span class="dl">"</span><span class="p">,</span>
        <span class="p">[</span>
          <span class="dl">"</span><span class="s2">find-generic-password</span><span class="dl">"</span><span class="p">,</span>
          <span class="dl">"</span><span class="s2">-a</span><span class="dl">"</span><span class="p">,</span>
          <span class="dl">"</span><span class="s2">LOCAL_SECRETS_DOTENVX_KEY</span><span class="dl">"</span><span class="p">,</span>
          <span class="dl">"</span><span class="s2">-s</span><span class="dl">"</span><span class="p">,</span>
          <span class="dl">"</span><span class="s2">LOCAL_SECRETS_DOTENVX_KEY</span><span class="dl">"</span><span class="p">,</span>
          <span class="dl">"</span><span class="s2">-w</span><span class="dl">"</span><span class="p">,</span>
        <span class="p">],</span>
        <span class="p">{</span> <span class="na">encoding</span><span class="p">:</span> <span class="dl">"</span><span class="s2">utf-8</span><span class="dl">"</span> <span class="p">},</span>
      <span class="p">).</span><span class="nf">trim</span><span class="p">();</span>
    <span class="p">}</span> <span class="nf">catch </span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span>
        <span class="dl">"</span><span class="s2">Failed to read decryption key from macOS Keychain. </span><span class="dl">"</span> <span class="o">+</span>
          <span class="dl">"</span><span class="s2">Make sure the LOCAL_SECRETS_DOTENVX_KEY item exists. </span><span class="dl">"</span> <span class="o">+</span>
          <span class="dl">"</span><span class="s2">See scripts/store-keychain-key.sh for setup.</span><span class="dl">"</span><span class="p">,</span>
        <span class="p">{</span> <span class="na">cause</span><span class="p">:</span> <span class="nx">err</span> <span class="p">},</span>
      <span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nf">config</span><span class="p">({</span> <span class="na">path</span><span class="p">:</span> <span class="nx">localSecretsPath</span><span class="p">,</span> <span class="na">overload</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>

  <span class="c1">// The private key has done its job. Remove it from the environment so it</span>
  <span class="c1">// is not visible in process.env dumps or child process inheritance.</span>
  <span class="k">delete</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DOTENV_PRIVATE_KEY_LOCAL_SECRETS</span><span class="p">;</span>
<span class="p">}</span></code></pre>
</div>

<p>Three notes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">execFileSync</code> will throw on a non-zero exit, so the try/catch above turns
a cryptic child-process error into a clear setup instruction</li>
  <li>the <code class="language-plaintext highlighter-rouge">delete</code> at the end matters: once <code class="language-plaintext highlighter-rouge">dotenvx</code> has decrypted the secrets
into their individual env vars, the private key has no further purpose; leaving
it in <code class="language-plaintext highlighter-rouge">process.env</code> means any code that inspects the environment (logging,
diagnostics, error reporters) could leak the one key that decrypts the file</li>
  <li>do not log even partial key material during startup. That is easy to get
wrong when debugging the integration</li>
</ul>

<p>One thing this does <strong>not</strong> protect against: once your app is running, the
decrypted secrets are plain strings in <code class="language-plaintext highlighter-rouge">process.env</code>. Anyone who can attach a
Node debugger to your process can inspect memory directly.</p>

<p>Cross-process env snooping is more nuanced. On macOS 11+, SIP prevents
processes from reading other processes' environment variables, so this vector
is largely closed on a default macOS install. On Linux, <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/environ</code>
is still readable by any process running as the same user. Either way, this
pattern is about secrets at rest on disk, not secrets in a running process.</p>

<h2 id="nextjs-integration">Next.js integration</h2>

<blockquote>
  <p><strong>Update:</strong> For the standard Next.js setup, including deployments on Vercel, use the <a href="/docs/nextjs/">canonical <code class="language-plaintext highlighter-rouge">@dotenvx/next-env</code> guide</a>. The custom instrumentation below belongs to this article's local macOS Keychain workflow; it is not the recommended general Next.js integration or a Vercel deployment recipe.</p>
</blockquote>

<p>If you are using Next.js, you cannot just call <code class="language-plaintext highlighter-rouge">loadEnv()</code> from anywhere and
expect it to work. Next.js has its own env loading built in, and by the time
your application code runs, it has already resolved which variables are
available.</p>

<p>The right place to hook this in is <code class="language-plaintext highlighter-rouge">instrumentation.ts</code> (or <code class="language-plaintext highlighter-rouge">.js</code>). Next.js
calls the <code class="language-plaintext highlighter-rouge">register</code> function exported from this file once when the server
starts, before any routes or middleware run. That makes it the earliest
reliable point to pull secrets from Keychain and inject them into <code class="language-plaintext highlighter-rouge">process.env</code>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c1">// instrumentation.ts</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">register</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">loadEnv</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">./lib/load-env</span><span class="dl">"</span><span class="p">);</span>
  <span class="nf">loadEnv</span><span class="p">();</span>
<span class="p">}</span></code></pre>
</div>

<p>The dynamic import is intentional. It keeps the Keychain and <code class="language-plaintext highlighter-rouge">dotenvx</code> logic
out of the client bundle and avoids top-level side effects that could run at
the wrong time.</p>

<p>Make sure <code class="language-plaintext highlighter-rouge">instrumentation.ts</code> is at your project root (next to <code class="language-plaintext highlighter-rouge">next.config</code>),
and that you are on Next.js 15+ where the instrumentation hook is stable. On
older versions (13.2 through 14.x) it works but requires setting
<code class="language-plaintext highlighter-rouge">experimental.instrumentationHook: true</code> in your Next config.</p>

<h2 id="helper-scripts-for-temporary-decryptre-encrypt">Helper scripts for temporary decrypt/re-encrypt</h2>

<p>I also like keeping two tiny helper scripts around so I can temporarily decrypt
the file, edit it, and then re-encrypt it.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># scripts/decrypt-local-secrets.sh</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nv">KEYCHAIN_ITEM_NAME</span><span class="o">=</span><span class="s2">"LOCAL_SECRETS_DOTENVX_KEY"</span>

<span class="nb">export </span><span class="nv">DOTENV_PRIVATE_KEY_LOCAL_SECRETS</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>
  security find-generic-password <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"</span><span class="nv">$KEYCHAIN_ITEM_NAME</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$KEYCHAIN_ITEM_NAME</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-w</span>
<span class="si">)</span><span class="s2">"</span>

pnpm <span class="nb">exec </span>dotenvx decrypt <span class="nt">-f</span> .env.local.secrets</code></pre>
</div>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># scripts/encrypt-local-secrets.sh</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nv">KEYCHAIN_ITEM_NAME</span><span class="o">=</span><span class="s2">"LOCAL_SECRETS_DOTENVX_KEY"</span>

<span class="nb">export </span><span class="nv">DOTENV_PRIVATE_KEY_LOCAL_SECRETS</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>
  security find-generic-password <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"</span><span class="nv">$KEYCHAIN_ITEM_NAME</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$KEYCHAIN_ITEM_NAME</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-w</span>
<span class="si">)</span><span class="s2">"</span>

pnpm <span class="nb">exec </span>dotenvx encrypt <span class="nt">-f</span> .env.local.secrets</code></pre>
</div>

<p>That gives you a simple workflow:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>bash scripts/decrypt-local-secrets.sh
<span class="c"># edit .env.local.secrets</span>
bash scripts/encrypt-local-secrets.sh</code></pre>
</div>

<p>One risk here: if you decrypt the file, edit it, and forget to re-encrypt,
your secrets are back to sitting in plaintext. A git pre-commit hook can catch
this. Something like:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># .husky/pre-commit or .git/hooks/pre-commit</span>
<span class="k">if </span><span class="nb">head</span> <span class="nt">-c</span> 50 .env.local.secrets 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-qv</span> <span class="s2">"^#/"</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"ERROR: .env.local.secrets appears to be decrypted. Run:"</span>
  <span class="nb">echo</span> <span class="s2">"  bash scripts/encrypt-local-secrets.sh"</span>
  <span class="nb">exit </span>1
<span class="k">fi</span></code></pre>
</div>

<p>(<code class="language-plaintext highlighter-rouge">dotenvx</code>-encrypted files start with a comment header like
<code class="language-plaintext highlighter-rouge">#/-------------------[DOTENV]--------------------/</code>, so checking for the
absence of that prefix is a reasonable heuristic.)</p>

<h2 id="where-this-helps-and-where-it-doesnt">Where this helps, and where it doesn't</h2>

<p>This pattern helps with:</p>

<ul>
  <li>raising the cost of supply chain attacks that look for <code class="language-plaintext highlighter-rouge">.env</code> files</li>
  <li>repo-wide local file scraping</li>
  <li>accidental plaintext secret exposure in local tooling</li>
  <li>reducing how many places secrets live on disk</li>
  <li>avoiding accidental exposure during screen sharing and pair programming</li>
</ul>

<p>It does not solve:</p>

<ul>
  <li>malicious code running inside your process</li>
  <li>a debugger attached to your Node process (secrets are in memory as plain strings)</li>
  <li>cross-process env snooping on Linux (<code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/environ</code>); macOS SIP blocks
this since Big Sur, but Linux does not</li>
  <li>secrets already exported into your shell</li>
  <li>logs or copy/paste leaks</li>
  <li>production secret management</li>
</ul>

<p>Think of it as one useful layer, not as a silver bullet.</p>

<h3 id="a-note-on-screen-sharing">A note on screen sharing</h3>

<p>If your secrets live in a
plain-text <code class="language-plaintext highlighter-rouge">.env.local</code>, it is very easy to accidentally flash them on screen
during a Zoom call, a pair programming session, or a live demo. All it takes
is opening the wrong file, running <code class="language-plaintext highlighter-rouge">cat</code> on it, or having your editor preview
it in a sidebar.</p>

<p>With encrypted <code class="language-plaintext highlighter-rouge">.env.local.secrets</code>, that file is just opaque ciphertext. Even
if you open it on camera, nobody sees your database credentials or API keys.
The decryption only happens at runtime, in memory, when your app starts, not
when a human or a screen recording is looking at your filesystem.</p>

<p>This is not a reason to adopt the pattern on its own, but it is a nice side
effect that has already saved me at least once.</p>

<h2 id="the-developer-experience-bar-matters">The developer-experience bar matters</h2>

<p>The reason I like this approach is that it is security work people may actually
keep using.</p>

<p>Once set up, the workflow is close to normal local development:</p>

<ul>
  <li>keep config in <code class="language-plaintext highlighter-rouge">.env.local</code></li>
  <li>keep secrets in <code class="language-plaintext highlighter-rouge">.env.local.secrets</code></li>
  <li>let the app pull the key from Keychain when needed</li>
</ul>

<p>That is much more likely to stick than a system that feels "more secure" on paper
but creates enough friction that everyone bypasses it.</p>

<h2 id="if-you-want-to-adopt-this">If you want to adopt this</h2>

<p>My suggestions:</p>

<ol>
  <li>Start with local-only encryption, not a big secret-sharing redesign.</li>
  <li>Separate non-secret config from secrets first.</li>
  <li>Make the Keychain path opt-in with a clear env flag.</li>
  <li>Ensure <code class="language-plaintext highlighter-rouge">.env.local</code> loads before you evaluate that flag.</li>
  <li>Audit helper scripts too, not just the main app boot path.</li>
  <li>Back up your decryption key in a password manager before deleting <code class="language-plaintext highlighter-rouge">.env.keys</code>.</li>
  <li>Add a pre-commit hook to catch unencrypted secrets files.</li>
  <li>Never print keys, even partially, while debugging the integration.</li>
</ol>

<p>That last point deserves repeating.</p>

<p>Security improvements have a way of being partially undone by "temporary"
debugging statements.</p>

<h2 id="related-tools-worth-looking-at">Related tools worth looking at</h2>

<p>If this pattern feels too lightweight for your needs, or you want something
more structured, there are good adjacent tools in this space.</p>

<p><a href="https://getsops.io/">SOPS</a> is a strong option when you want encrypted files as
a first-class workflow, especially in teams already comfortable with cloud KMS,
age, or GitOps-style config management.</p>

<p><a href="https://dmno.dev/">DMNO</a> goes in a different direction: schema-aware config,
tooling around developer experience, and integrations with external secret
stores. Their <a href="https://dmno.dev/docs/plugins/1password/">1Password plugin</a>
is a good example if you want local development ergonomics tied more directly to
a secrets manager instead of local encrypted <code class="language-plaintext highlighter-rouge">.env</code> files.</p>

<p>I do not see these as mutually exclusive with the smaller pattern in this post.
They just sit at different points on the complexity and capability curve.</p>

<h2 id="closing-thought">Closing thought</h2>

<p>I do not think local <code class="language-plaintext highlighter-rouge">.env</code> files are going away anytime soon.</p>

<p>But I do think the threat model around them has changed.</p>

<p>A small amount of structure, encryption at rest, and OS-managed key storage can
go a surprisingly long way without making local development miserable.</p>

<p><strong>Followup</strong>: I also wrote a companion script that scans your machine for plaintext secrets sitting in .env files. It pairs well with this post as a way to find what needs encrypting.
<a href="https://dev.to/ustun/find-plaintext-secrets-hiding-in-your-env-files-5dpl">Find Plaintext Secrets Hiding in Your .env Files</a></p>]]></content><author><name>Ustun Ozgur</name></author><category term="blog" /><summary type="html"><![CDATA[A defense layer against increasing supply chain attacks that read your .env files]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2026-04-02-dotenvx-keychain-html-ab00ef48b25012d5.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2026-04-02-dotenvx-keychain-html-ab00ef48b25012d5.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Stop .env Drift Before Merge with Wizard of Drift</title><link href="https://dotenvx.com/blog/2026/03/02/wizard-of-drift-oz-and-dotenvx.html" rel="alternate" type="text/html" title="Stop .env Drift Before Merge with Wizard of Drift" /><published>2026-03-02T00:00:00+00:00</published><updated>2026-03-02T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2026/03/02/wizard-of-drift-oz-and-dotenvx</id><content type="html" xml:base="https://dotenvx.com/blog/2026/03/02/wizard-of-drift-oz-and-dotenvx.html"><![CDATA[<p>I shipped a small GitHub Action this week: <a href="https://github.com/dotenvx/wizard-of-drift">wizard-of-drift</a>.</p>

<p>It catches <code class="language-plaintext highlighter-rouge">.env*</code> key drift in pull requests and leaves a review comment with concrete fixes.</p>

<p>It uses <a href="https://warp.dev/oz">Warp's Oz</a> under the hood to do the review, and it works great.</p>

<p><img src="https://github.com/user-attachments/assets/b77d8c0f-a96e-4e31-905d-6c76c42882a9" /></p>

<h2 id="why">Why</h2>

<p>Teams with multiple env files (<code class="language-plaintext highlighter-rouge">.env</code>, <code class="language-plaintext highlighter-rouge">.env.production</code>, <code class="language-plaintext highlighter-rouge">.env.staging</code>, etc) slowly drift.</p>

<p>Someone adds <code class="language-plaintext highlighter-rouge">TWILIO_API_KEY</code> to one file and forgets the others. CI passes, deploy goes out, and then something breaks in preview or prod.</p>

<p>Wizard of Drift catches that in the PR before merge.</p>

<h2 id="the-real-problem">The Real Problem</h2>

<p>Most env drift bugs are boring and expensive:</p>

<ul>
  <li>local works, preview fails</li>
  <li>preview works, production fails</li>
  <li>one service has a key, another service does not</li>
</ul>

<p>And they are hard to catch in code review because reviewers are focused on app code, not checking every <code class="language-plaintext highlighter-rouge">.env*</code> file by hand.</p>

<p>Even worse, the PR diff can hide this. If a new key is added in <code class="language-plaintext highlighter-rouge">.env.production</code> only, nobody notices until a runtime path hits missing config.</p>

<p>This is the kind of failure that burns hours for no good reason.</p>

<h2 id="what-it-checks">What It Checks</h2>

<ul>
  <li>Scans <code class="language-plaintext highlighter-rouge">.env*</code> files in the repo</li>
  <li>Excludes <code class="language-plaintext highlighter-rouge">.env.keys</code> (never commit that)</li>
  <li>Compares key names only (not values)</li>
  <li>Handles <code class="language-plaintext highlighter-rouge">DOTENV_PUBLIC_KEY</code> naming rules per file:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">.env</code> expects <code class="language-plaintext highlighter-rouge">DOTENV_PUBLIC_KEY</code></li>
      <li><code class="language-plaintext highlighter-rouge">.env.&lt;target&gt;</code> expects <code class="language-plaintext highlighter-rouge">DOTENV_PUBLIC_KEY_&lt;TARGET_UPPERCASE&gt;</code></li>
    </ul>
  </li>
  <li>Posts a PR review summary with missing key lines to add</li>
</ul>

<p><img src="https://github.com/user-attachments/assets/a15a1e49-ac63-47a4-83ff-c71f03fddf83" /></p>

<h2 id="add-it-in-30-seconds">Add It In 30 Seconds</h2>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Env Drift Review</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">types</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">opened</span><span class="pi">,</span> <span class="nv">synchronize</span><span class="pi">,</span> <span class="nv">reopened</span><span class="pi">]</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
  <span class="na">pull-requests</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">env-drift</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">dotenvx/wizard-of-drift@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">warp_api_key</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">github_token</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">warp_agent_profile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span> <span class="c1"># optional</span></code></pre>
</div>

<h2 id="why-a-coding-agent-works-well-here">Why A Coding Agent Works Well Here</h2>

<p>This is a great use case for an agent because the input is high-context but bounded:</p>

<ul>
  <li>list of env files</li>
  <li>extracted key sets</li>
  <li>the <code class="language-plaintext highlighter-rouge">.env*</code> diff in the PR</li>
  <li>explicit rules about <code class="language-plaintext highlighter-rouge">DOTENV_PUBLIC_KEY</code> naming</li>
</ul>

<p>Wizard of Drift builds that context first, then gives the agent (in this case Oz) a tight prompt and asks for one output: a concise review summary with exact keys to add.</p>

<p>That pattern matters.</p>

<p>The agent is not guessing from vague code context. It is reviewing a purpose-built context document generated by CI, then returning actionable output directly into the PR conversation.</p>

<p>So the reviewer sees concrete fixes, not generic AI advice.</p>

<h2 id="the-bigger-idea">The Bigger Idea</h2>

<p>This is the kind of workflow I want more of:</p>

<ul>
  <li>static CI context</li>
  <li>AI agent review</li>
  <li>deterministic output in PR comments</li>
</ul>

<p>And Oz was a great choice here because I wanted to easily trigger a coding agent from a GitHub action. Plus it supports any model and can be monitored from <a href="https://oz.warp.dev">its dashboard</a>.</p>

<p>The combination makes my <code class="language-plaintext highlighter-rouge">.env</code> workflows safer and easier to operate at scale. Dotenvx gives you the secure env model and the agent gives you a practical enforcement loop at review time.</p>

<p>Together, they remove a class of annoying config breakages before they merge.</p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[Catch .env key drift on pull requests automatically with Warp's Oz + Dotenvx.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2026-03-02-wizard-of-drift-oz-and-dotenvx-html-a94bf9f0054c8551.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2026-03-02-wizard-of-drift-oz-and-dotenvx-html-a94bf9f0054c8551.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Setting Up dotenvx with Next.js</title><link href="https://dotenvx.com/blog/2026/02/17/dotenvx-nextjs.html" rel="alternate" type="text/html" title="Setting Up dotenvx with Next.js" /><published>2026-02-17T00:00:00+00:00</published><updated>2026-02-17T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2026/02/17/dotenvx-nextjs</id><content type="html" xml:base="https://dotenvx.com/blog/2026/02/17/dotenvx-nextjs.html"><![CDATA[<aside aria-labelledby="nextjs-update-warning">
  <h2 id="nextjs-update-warning">⚠️ Use the new Next.js approach</h2>
  <p><strong>This article's original setup has been superseded.</strong> Use <code>@dotenvx/next-env</code> for Next.js, including apps deployed on Vercel. It replaces the earlier CLI wrapper and instrumentation approach.</p>
  <p><a href="/docs/nextjs/">Follow the current Next.js setup →</a></p>
</aside>

<p><em>By <a href="https://tonyvantur.com">Tony Vantur</a></em></p>

<p>Dotenvx encrypts your <code class="language-plaintext highlighter-rouge">.env</code> files so you can commit them to git. Your app reads decrypted values through <code class="language-plaintext highlighter-rouge">process.env</code>, while the private decryption keys stay outside your repository.</p>

<h2 id="set-up-nextjs">Set up Next.js</h2>

<p>Follow the <a href="/docs/nextjs/">canonical Next.js guide</a> to install <code class="language-plaintext highlighter-rouge">@dotenvx/dotenvx</code> and <code class="language-plaintext highlighter-rouge">@dotenvx/next-env</code>, add the <code class="language-plaintext highlighter-rouge">@next/env</code> override, and encrypt your <code class="language-plaintext highlighter-rouge">.env</code> file. The guide includes the current configuration and troubleshooting steps.</p>

<p>Use your normal Next.js scripts (<code class="language-plaintext highlighter-rouge">next dev</code>, <code class="language-plaintext highlighter-rouge">next build</code>, and <code class="language-plaintext highlighter-rouge">next start</code>) and continue reading variables with <code class="language-plaintext highlighter-rouge">process.env</code>.</p>

<h2 id="deploy-on-vercel">Deploy on Vercel</h2>

<p>Use the same <a href="/docs/nextjs/">canonical Next.js integration</a> on Vercel. Commit encrypted environment files, keep <code class="language-plaintext highlighter-rouge">.env.keys</code> out of deployment uploads, and configure the matching private keys in Vercel.</p>

<p>Configure keys for each Vercel environment you deploy to. Preview builds also use <code class="language-plaintext highlighter-rouge">NODE_ENV=production</code>, so Vercel's Preview scope does not automatically select a different Next.js env file.</p>

<h2 id="migrating-from-the-earlier-instructions">Migrating from the earlier instructions</h2>

<p>After applying the canonical setup:</p>

<ul>
  <li>Remove <code class="language-plaintext highlighter-rouge">dotenvx run --</code> wrappers from your Next.js scripts.</li>
  <li>Remove instrumentation code added solely to call <code class="language-plaintext highlighter-rouge">dotenvx.config()</code>. Keep instrumentation used for other purposes.</li>
  <li>Replace any <code class="language-plaintext highlighter-rouge">dotenvx.get</code> calls introduced by the old Vercel guide with <code class="language-plaintext highlighter-rouge">process.env</code> reads.</li>
  <li>Keep your encrypted env files and configure their matching decryption keys on Vercel, then redeploy.</li>
</ul>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="/docs/nextjs/">Next.js guide</a> — Canonical integration with <code class="language-plaintext highlighter-rouge">@dotenvx/next-env</code></li>
  <li><a href="/docs">Dotenvx documentation</a> — CLI and SDK reference</li>
</ul>]]></content><author><name>Tony Vantur</name></author><category term="blog" /><summary type="html"><![CDATA[Use the canonical @dotenvx/next-env setup for Next.js, locally and on Vercel.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2026-02-17-dotenvx-nextjs-html-0b4bff4850df8baa.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2026-02-17-dotenvx-nextjs-html-0b4bff4850df8baa.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rotate NPM Tokens with Dotenvx Armor ⛨. Automatically.</title><link href="https://dotenvx.com/blog/2025/12/11/rotate-npm-tokens-with-dotenvx-ops.html" rel="alternate" type="text/html" title="Rotate NPM Tokens with Dotenvx Armor ⛨. Automatically." /><published>2025-12-11T00:00:00+00:00</published><updated>2025-12-11T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2025/12/11/rotate-npm-tokens-with-dotenvx-ops</id><content type="html" xml:base="https://dotenvx.com/blog/2025/12/11/rotate-npm-tokens-with-dotenvx-ops.html"><![CDATA[<p><strong>NPM's new short-lived tokens</strong> strengthen security, but they <strong>make rotation painful.</strong> Every 90 days (or sooner) you have to manually create a fresh token, set it in your CI, and make sure nothing breaks. <sup><a href="#footnote1">1</a></sup></p>

<p><img src="https://github.com/user-attachments/assets/9868574d-2e81-4654-b4bc-b1c66df19784" /></p>

<p>This was a real problem for us. We publish <a href="https://www.npmjs.com/org/dotenvx">64 npm packages</a>, and rotating tokens across all of them by hand was not going to be sustainable. Every expiration meant touching dozens of pipelines and praying the next publish didn't fail.</p>

<p>So we built a solution. Introducing <a href="https://dotenvx.com/docs/armor/rotate">Dotenvx Rotate</a> - part of Dotenvx Armor ⛨.</p>

<h2 id="how-it-works">How It Works</h2>

<p>Install <a href="https://dotenvx.com/armor">dotenvx-armor</a>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>curl <span class="nt">-sfS</span> https://dotenvx.com/armor | sh</code></pre>
</div>

<p>Run <code class="language-plaintext highlighter-rouge">rotate npm connect</code> to connect <a href="https://npmjs.com">npm</a>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor rotate npm connect</code></pre>
</div>

<p>When prompted enter your npm username and password.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor rotate npm connect
✔ npm username: USERNAME
✔ npm password: PASSWORD</code></pre>
</div>

<p>This opens a local browser session, connecting your npm account.</p>

<p><img src="https://github.com/user-attachments/assets/49ee113b-95dd-4586-87ca-06da3b0f8d20" /></p>

<blockquote>
  <p>IMPORTANT: Note that this is <strong>local only</strong> - this way we can bypass the need for storing your credentials.</p>
</blockquote>

<p>Complete any 2FA steps manually.</p>

<p><img src="https://github.com/user-attachments/assets/a7487c65-0ee6-4415-9a94-d1e64a377e66" /></p>

<p>On success, return to your CLI, and you will see a passcard created.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor rotate npm connect
✔ npm username: USERNAME
✔ npm password: PASSWORD
✔ connected <span class="o">[</span>https://armor.dotenvx.com/go/pas_1234..]</code></pre>
</div>

<p><em>Dotenvx Passcards</em> are special connectors allowing account access.</p>

<p>Next, use the passcard to rotate your npm token.</p>

<h2 id="rotate">Rotate</h2>

<p>Run <code class="language-plaintext highlighter-rouge">rotate</code> on the passcard.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor rotate dotenvx://pas_1234..
⠏ rotating..</code></pre>
</div>

<p>It takes 10-30 seconds. On success, it returns a Dotenvx Rotation Token (ROT).</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor rotate dotenvx://pas_1234..
✔ rotated <span class="o">[</span>https://armor.dotenvx.com/go/pas_1234..]
⮕ next run <span class="o">[</span>dotenvx-armor get dotenvx://rot_a2c4..]</code></pre>
</div>

<p><em>Dotenvx Rotation Tokens (ROTs)</em> are special tokens that can change value. You can think of them as proxy tokens.</p>

<p>Next, let's get the value for it.</p>

<h2 id="get">Get</h2>

<p>Run <code class="language-plaintext highlighter-rouge">get</code> on the rotation token.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor get dotenvx://rot_a2c4..
npm_d2cJ..</code></pre>
</div>

<p>It returns your npm token. Cool!</p>

<h2 id="rotate-again">Rotate Again</h2>

<p>Run <code class="language-plaintext highlighter-rouge">rotate</code> on the passcard again.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor rotate dotenvx://pas_1234..
✔ rotated <span class="o">[</span>https://armor.dotenvx.com/go/pas_1234..]
⮕ next run <span class="o">[</span>dotenvx-armor get dotenvx://rot_a2c4..]</code></pre>
</div>

<p>And <code class="language-plaintext highlighter-rouge">get</code> the ROT again.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="nv">$ </span>dotenvx-armor get dotenvx://rot_a2c4..
npm_cbGY..</code></pre>
</div>

<p>The value changed. Way cool!</p>

<p>That's the ROT at work. ROTs introduce a new key rotation primitive: the npm token rotates, the reference does not. This is useful for operations, especially CI/CD.</p>

<h2 id="cicd">CI/CD</h2>

<p>Previously, our CI/CD had <code class="language-plaintext highlighter-rouge">npm publish</code> with a hardcoded <code class="language-plaintext highlighter-rouge">secrets.NPM_TOKEN</code>:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="na">npm</span><span class="pi">:</span>
  <span class="s">...</span>
  <span class="s">runs-on</span><span class="err">:</span> <span class="s">ubuntu-latest</span>
  <span class="s">steps</span><span class="err">:</span>
    <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
    <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v4</span>
      <span class="na">with</span><span class="pi">:</span>
        <span class="na">node-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">18.x'</span>
        <span class="na">registry-url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://registry.npmjs.org'</span>
    <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm publish</span>
      <span class="na">env</span><span class="pi">:</span>
        <span class="na">NODE_AUTH_TOKEN</span><span class="pi">:</span> <span class="s">${{ secrets.NPM_TOKEN }}</span></code></pre>
</div>

<h4 id="step-1">Step 1</h4>

<p>We first replaced <code class="language-plaintext highlighter-rouge">secrets.NPM_TOKEN</code> with <code class="language-plaintext highlighter-rouge">env.NODE_AUTH_TOKEN</code>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="na">npm</span><span class="pi">:</span>
  <span class="s">...</span>
  <span class="s">runs-on</span><span class="err">:</span> <span class="s">ubuntu-latest</span>
  <span class="s">steps</span><span class="err">:</span>
    <span class="s">...</span>
    <span class="s">- run</span><span class="err">:</span> <span class="s">npm publish</span>
      <span class="s">env</span><span class="err">:</span>
        <span class="na">NODE_AUTH_TOKEN</span><span class="pi">:</span> <span class="s">${{ env.NODE_AUTH_TOKEN }}</span></code></pre>
</div>

<h4 id="step-2">Step 2</h4>

<p>Then we added a step to:</p>

<ul>
  <li><a href="https://dotenvx.com/docs/armor/install">Install dotenvx-armor</a></li>
  <li>Run <code class="language-plaintext highlighter-rouge">dotenvx-armor get</code> to echo its value to <code class="language-plaintext highlighter-rouge">NODE_AUTH_TOKEN</code></li>
</ul>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code><span class="na">npm</span><span class="pi">:</span>
  <span class="s">...</span>
  <span class="s">runs-on</span><span class="err">:</span> <span class="s">ubuntu-latest</span>
  <span class="s">steps</span><span class="err">:</span>
    <span class="s">...</span>
    <span class="s">- run</span><span class="err">:</span> <span class="pi">|</span>
        <span class="s">curl -sfS https://dotenvx.sh/armor | sh</span>
        <span class="s">echo "NODE_AUTH_TOKEN=$(dotenvx-armor get dotenvx://rot_a2c4 --token '${{ secrets.DOTENVX_ARMOR_TOKEN }}')" &gt;&gt; $GITHUB_ENV</span>
    <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm publish</span>
      <span class="na">env</span><span class="pi">:</span>
        <span class="na">NODE_AUTH_TOKEN</span><span class="pi">:</span> <span class="s">${{ env.NODE_AUTH_TOKEN }}</span></code></pre>
</div>

<h4 id="step-3">Step 3</h4>

<p>Last, we set <code class="language-plaintext highlighter-rouge">DOTENVX_ARMOR_TOKEN</code> in <a href="https://github.com/username/project/settings/secrets/actions">GitHub Actions Secrets</a> (or GitLab CI, CircleCI, or wherever you run your automated npm publishing).</p>

<p><img src="https://github.com/user-attachments/assets/db12882b-8b35-40db-a62f-238df32ff3f6" /></p>

<p>Tip: Find your <code class="language-plaintext highlighter-rouge">DOTENVX_ARMOR_TOKEN</code> on your <a href="https://armor.dotenvx.com/settings">Dotenvx Settings Page</a>.</p>

<p><img src="https://github.com/user-attachments/assets/df4f6146-5cf9-44a7-9a22-b967d675f3d8" /></p>

<p>On your next CI run, it will inject the latest rotated NPM token and successfully publish your npm module(s).</p>

<p><img src="https://github.com/user-attachments/assets/7a201f23-c255-4d75-a46e-326ddf22f0d9" /></p>

<p>Incredible!</p>

<h2 id="conclusion">Conclusion</h2>

<p>Publishing now works indefinitely with rotating NPM tokens, powered by a new rotation primitive (ROTs) and passcard connectors.</p>

<ul>
  <li><em>NPM token leaked?</em> Just rotate it - all your operations still work.</li>
  <li><em>Employee left who knew the old token?</em> Rotate it - all your operations still work.</li>
  <li><em>NPM token should be rotated every N days for compliance?</em> Put it on a schedule - all your operations still work.</li>
</ul>

<p>This has worked really well for us. If it sounds useful, you can use it too. Sign up for <a href="https://dotenvx.com/armor">Dotenvx Armor ⛨</a>.</p>

<blockquote>
  <p>P.S. If you're running this at enterprise scale with compliance requirements, scheduled rotation, or broader CI/CD concerns, please <a href="mailto:scott@dotenvx.com">get in touch</a>. We'd like to help.</p>
</blockquote>

<hr />

<p><small><sup id="footnote1">1</sup> <a href="https://github.blog/changelog/2025-09-29-strengthening-npm-security-important-changes-to-authentication-and-token-management/">Strengthening npm security: Important changes to authentication and token management</a></small></p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[NPM's new short-lived tokens strengthen security, but they make rotation painful.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2025-12-11-rotate-npm-tokens-with-dotenvx-ops-html-d3ae5e3bde51616b.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2025-12-11-rotate-npm-tokens-with-dotenvx-ops-html-d3ae5e3bde51616b.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Dotenvx vs Docker Compose Secrets: Avoiding False Security</title><link href="https://dotenvx.com/blog/2025/04/19/dotenvx-vs-docker-compose-secrets.html" rel="alternate" type="text/html" title="Dotenvx vs Docker Compose Secrets: Avoiding False Security" /><published>2025-04-19T00:00:00+00:00</published><updated>2025-04-19T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2025/04/19/dotenvx-vs-docker-compose-secrets</id><content type="html" xml:base="https://dotenvx.com/blog/2025/04/19/dotenvx-vs-docker-compose-secrets.html"><![CDATA[<p><strong>Managing secrets in containers can be tricky.</strong> Many developers have heard that environment variables might expose secrets, so using <a href="https://docs.docker.com/compose/how-tos/use-secrets/">Docker Compose Secrets</a> sounds safer. But is it?</p>

<p>In practice, Docker Compose secrets are just plaintext files in disguise, which introduces its own risks. Meanwhile, <a href="https://github.com/dotenvx/dotenvx">dotenvx</a> takes a different approach with just-in-time secret injection that avoids leaving sensitive data lying around.</p>

<h2 id="good-idea-unencrypted-reality">Good Idea, Unencrypted Reality</h2>

<p>Docker Compose added a feature called <em>secrets</em> to keep passwords and API keys out of your Dockerfiles and environment variables. The idea sounds good. But here's the catch: those secrets are <strong>just plaintext files</strong>.</p>

<p>Compose bind-mounts a plaintext file into your container under <code class="language-plaintext highlighter-rouge">/run/secrets</code> <sup><a href="#footnote1">1</a></sup>. No encryption.</p>

<p><a href="https://docs.docker.com/compose/how-tos/use-secrets/" target="_blank"><img src="https://github.com/user-attachments/assets/eedb7ab3-9c9a-49b2-9583-ec11ad411783" /></a></p>

<p>As a result, in a running container, an attacker (or malicious process) can simply read the unencrypted file from <code class="language-plaintext highlighter-rouge">/run/secrets</code> and obtain your secret – these secret files are mounted world-readable (mode 0444) <sup><a href="#footnote1">1</a></sup>.</p>

<h2 id="plaintext-secrets-at-rest">Plaintext Secrets at Rest</h2>

<p>Why is a plaintext file at rest such a big deal? Because anything stored on disk is one stray commit or backup away from exposure. If you check that secret file into source control by mistake, or if your server gets compromised, the secret is sitting there in plain text.</p>

<p>Docker Compose avoids putting secrets in images or environment variables, but leaving them on the filesystem means they persist longer than they might need to. <strong>Long-lived plaintext secrets are an inviting target.</strong></p>

<p>For example, a common slip-up is a developer accidentally committing a .env or secret file to GitHub – now your passwords are public. Even on a server, a misconfigured web route could unintentionally serve that file, or an attacker could find it among backups.</p>

<p>A secret that lives as plaintext on disk is always at risk of being read by someone unauthorized.</p>

<h2 id="environment-variables">Environment Variables</h2>

<p>On the other hand, <strong>environment variables vanish once the process stops and aren't directly saved to disk</strong>. But environment variables have their own pitfalls if misused. If you print them in logs or leave them in an .env file on disk, you're back to square one.</p>

<p>The takeaway here is that <em>how you handle the secret matters more than the mechanism. Simply moving a secret from an env var to a file doesn't automatically make it safe.</em></p>

<h2 id="dotenvx-just-in-time-secrets-injection">Dotenvx: Just-in-Time Secrets Injection</h2>

<p>Dotenvx approaches secret management with a focus on minimal exposure. It injects your secrets at runtime only, via the command <code class="language-plaintext highlighter-rouge">dotenvx run</code>. How does this help?</p>

<p>First, it means <strong>you don't leave secret values sitting around in a container's environment or filesystem</strong> for longer than necessary. The dotenvx CLI loads the secrets from an encrypted <code class="language-plaintext highlighter-rouge">.env</code> file and injects them as environment variables only while launching your app, then your app uses them in-memory.</p>

<p>There's no separate plaintext secret file hanging around permanently. In CI/CD, for example, dotenvx will decrypt your secrets and inject them "just in time" as the build or app starts.</p>

<h2 id="dotenvx-encryption">Dotenvx: Encryption</h2>

<p><strong>Crucially, dotenvx lets you encrypt your .env files.</strong> With one command <code class="language-plaintext highlighter-rouge">dotenvx encrypt</code>, you transform your <code class="language-plaintext highlighter-rouge">.env</code> into an encrypted format.</p>

<p>Even if someone finds that file, they can't read the secrets without the decryption key. It uses AES-256 encryption with ephemeral keys so that even if the encrypted .env file is exposed, its contents remain secure. <sup><a href="#footnote3">3</a></sup> You can commit the encrypted .env to your repo safely – it's just gibberish to anyone without the key.</p>

<p>Come runtime, you provide the key (often via an environment variable or a secret manager) and dotenvx seamlessly decrypts and injects the real values into your app. The end result: <em>no plaintext secrets sitting at rest on your disk or in your container. They exist only in memory when needed.</em></p>

<h2 id="how-it-works">How It Works</h2>

<p>You run your app like this:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>$ dotenvx run -- node app.js</code></pre>
</div>

<p>Secrets are decrypted and loaded as environment variables <em>just for that process</em>. Nothing written to disk. No lingering files in containers.</p>

<p>Want to commit secrets to your repo? Encrypt them first:</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>$ dotenvx encrypt</code></pre>
</div>

<p>Then you can safely version your encrypted <code class="language-plaintext highlighter-rouge">.env</code> file. It's useless without the decryption key.</p>

<p>It's that easy and no plaintext files sitting around! Great!</p>

<h2 id="comparison">Comparison</h2>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Docker Compose Secrets</th>
      <th>Dotenvx</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Secrets encrypted at rest?</td>
      <td>❌ No</td>
      <td>✅ Yes</td>
    </tr>
    <tr>
      <td>Secrets exist on disk?</td>
      <td>✅ Yes (plaintext file)</td>
      <td>❌ No (only in memory)</td>
    </tr>
    <tr>
      <td>Easy to leak by accident?</td>
      <td>✅ Yes (file is always there)</td>
      <td>🚫 Much harder (encrypted, ephemeral)</td>
    </tr>
    <tr>
      <td>Can safely commit secrets?</td>
      <td>❌ No</td>
      <td>✅ Yes (if encrypted)</td>
    </tr>
    <tr>
      <td>Works outside containers?</td>
      <td>❌ Not easily</td>
      <td>✅ Yes</td>
    </tr>
  </tbody>
</table>

<h2 id="takeaway">Takeaway</h2>

<p>Docker Compose secrets <em>look</em> safer than env vars — but they aren't encrypted, and they persist longer. Meanwhile, dotenvx focuses on <strong>short-lived, encrypted, just-in-time secrets</strong>.</p>

<p>If you care about reducing blast radius and limiting exposure, <a href="https://github.com/dotenvx/dotenvx">dotenvx</a> is a solid and modern option to consider.</p>

<p>To learn more, <a href="https://dotenvx.com/dotenvx.pdf">read the whitepaper</a>.</p>

<hr />

<p><small><sup id="footnote1">1</sup> <a href="https://docs.docker.com/reference/compose-file/configs/">docker.com</a></small>
<small><sup id="footnote2">2</sup> <a href="https://news.ycombinator.com/item?id=40798534">news.ycombinator.com</a></small>
<small><sup id="footnote3">3</sup> <a href="https://github.com/dotenvx/dotenvx">github.com/dotenvx/dotenvx</a></small></p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[Why secrets in plaintext files might be more dangerous than environment variables — and how dotenvx helps.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2025-04-19-dotenvx-vs-docker-compose-secrets-html-ad7a4f74d8f34641.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2025-04-19-dotenvx-vs-docker-compose-secrets-html-ad7a4f74d8f34641.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Claude MCP + Dotenvx</title><link href="https://dotenvx.com/blog/2025/04/01/claude-mcp-dotenvx.html" rel="alternate" type="text/html" title="Claude MCP + Dotenvx" /><published>2025-04-01T00:00:00+00:00</published><updated>2025-04-01T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2025/04/01/claude-mcp-dotenvx</id><content type="html" xml:base="https://dotenvx.com/blog/2025/04/01/claude-mcp-dotenvx.html"><![CDATA[<p class="text-center small">Add Dotenvx to Claude as an MCP Server.</p>

<figure data-design-video="" class="design-video">
  <div class="design-video-frame">
    <video class="design-video-player" controls="" preload="metadata" playsinline="">
      
        <source src="https://github.com/user-attachments/assets/5f974de0-1831-4ae7-a3c8-a724418863db" type="video/mp4" />
      
      
      
        Download the <a class="design-link" href="https://github.com/user-attachments/assets/5f974de0-1831-4ae7-a3c8-a724418863db">mp4</a> video.
      
    </video>
    <button class="design-video-preview-play design-video-play" type="button" aria-label="Play Claude MCP with dotenvx" hidden="">
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v13.72L19.5 12 8 5.14z" /></svg>
    </button>
  </div>
  <figcaption class="design-video-title">Claude MCP with dotenvx</figcaption>
</figure>

<p>Dotenvx works with <a href="https://www.anthropic.com/news/model-context-protocol">Claude MCP</a>. Here's how to get it in your Claude.</p>

<h2 id="step-1">Step 1</h2>

<p>Type <code class="language-plaintext highlighter-rouge">claude mcp add</code>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>$ claude mcp add
Add MCP Server

Choose a unique name for this server:

&gt; dotenvx</code></pre>
</div>

<h2 id="step-2">Step 2</h2>

<p>For <em>Server Scope</em>, choose <em>User</em>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>Server Scope

Choose where this server will be available:

  
  Project (shared via .mcp.json)
  Local (private to you in this project)
&gt; User (available in all your projects)</code></pre>
</div>

<h2 id="step-3">Step 3</h2>

<p>For <em>Transport Type</em>, choose <em>Stdio</em>.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>Transport Type

Choose how Claude Code will connect to your MCP server:

&gt; Stdio (command-line process)
  SSE (Server-Sent Events over HTTP)</code></pre>
</div>

<h2 id="step-4">Step 4</h2>

<p>Enter <code class="language-plaintext highlighter-rouge">npx @dotenvx/dotenvx</code> as the server command.</p>

<div class="design-codeblock-wrap" data-design-codeblock>
<button type="button" class="design-codeblock-copy" data-design-codeblock-copy aria-label="Copy code"><svg class="design-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="design-copy-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg><span class="design-copy-status" data-design-codeblock-label aria-live="polite">Copy</span></button>
<pre class="design-codeblock"><code>Server Command

Enter the full command to start your MCP server.

&gt; npx @dotenvx/dotenvx</code></pre>
</div>

<p>Confirm that. That's it!</p>

<p>Now you can ask Claude to <em>encrypt my .env file</em> and it will do it!</p>

<blockquote>
  <p>Hey Claude, encrypt my .env file please.</p>
</blockquote>

<h2 id="bonus">Bonus</h2>

<p>Additionally, be sure to share <a href="https://dotenvx.com/llms.txt">llms.txt</a> and <a href="https://dotenvx.com/llms-full.txt">llms-full.txt</a> with Claude to make it smarter about all this.</p>

<p>Thanks for using Dotenvx.</p>

<hr />

<p>If you enjoyed this post, please <a href="https://github.com/dotenvx/dotenvx">share dotenvx with friends</a> or <a href="https://github.com/dotenvx/dotenvx">star it on GitHub</a> to help spread the word.</p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[Add Dotenvx as a Claude MCP Server.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2025-04-01-claude-mcp-dotenvx-html-660fdc37ddaa83be.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2025-04-01-claude-mcp-dotenvx-html-660fdc37ddaa83be.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">1 Million Installs: From Experimental to Essential</title><link href="https://dotenvx.com/blog/2025/03/03/one-million-installs.html" rel="alternate" type="text/html" title="1 Million Installs: From Experimental to Essential" /><published>2025-03-03T00:00:00+00:00</published><updated>2025-03-03T00:00:00+00:00</updated><id>https://dotenvx.com/blog/2025/03/03/one-million-installs</id><content type="html" xml:base="https://dotenvx.com/blog/2025/03/03/one-million-installs.html"><![CDATA[<p>Eight months ago, dotenvx introduced a new way to think about <a href="/blog/2024/06/24/dotenvx-next-generation-config-management.html">config management</a>. Today, it's an essential secrets tool with over <a href="https://www.npmjs.com/package/@dotenvx/dotenvx">1 million monthly installs</a> (and growing quickly).</p>

<p><a href="https://npm.chart.dev/@dotenvx/dotenvx" target="_blank"><img src="https://github.com/user-attachments/assets/776ed814-b2d0-49be-a61f-98ed494c7aa3" /></a></p>

<p>That growth didn't happen by accident—dotenvx development has been heavy with <a href="https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md#1383">88 releases in 8 months</a>. That's a new release every 2.75 days!</p>

<p>Today, it is a world-class tool–handling <a href="https://dotenvx.com/docs/quickstart">.env parsing</a>, <a href="https://dotenvx.com/docs/advanced/run-variable-expansion">variable expansion</a>, <a href="https://dotenvx.com/docs/advanced/run-command-substitution">command substitution</a>, <a href="https://dotenvx.com/docs/env-file#encryption">encrypted .env files</a>, and <a href="https://dotenvx.com/docs/advanced">more</a> better than anything else can. Each day, hundreds of software engineers newly learn of, and begin using, dotenvx.</p>

<p><img src="https://github.com/user-attachments/assets/1297f2d5-579d-4894-8bc2-91f0f90e4f91" /></p>

<p>As a result, it's starting to grow into something bigger—<strong>a community, an ecosystem, and a standard.</strong></p>

<h2 id="community">Community</h2>

<p>Dotenvx has grown fast, and a big part of that is developer involvement. In just eight months, engineers have contributed ideas, issues, and pull requests that shaped its direction.</p>

<ul>
  <li><strong>GitHub Activity.</strong> More than <a href="https://github.com/dotenvx/dotenvx/issues">250 issues filed</a>, <a href="https://github.com/dotenvx/dotenvx/pulls">275 PRs created</a>, and <a href="https://github.com/user-attachments/assets/1258fd84-165c-4947-b13b-ffeb49822860">60,000 lines of code written</a> in the last eight months.</li>
  <li><strong>Third-Party Documentation.</strong> Users <a href="https://medium.com/@t.dekiere/boost-dx-with-dotenvx-71e276dce6f6">wrote</a> <a href="https://parottasalna.com/2024/06/30/migrating-from-env-to-dotenvx/">about</a> <a href="https://dev.to/this-is-learning/exploring-dotenvx-46ng">dotenvx</a>, made <a href="https://youtu.be/xcBHX2m2pNw?t=102">videos</a> <a href="https://www.youtube.com/watch?v=1p2MS8rKHzU&amp;t=3s">about</a> <a href="https://www.youtube.com/watch?v=APhfQ2xya9A">dotenvx</a>, <a href="https://www.reddit.com/r/webdev/comments/1gteux5/comment/lxmhzue/">recommended</a> <a href="https://www.reddit.com/r/Python/comments/1gud1h9/comment/lxtv16s/">dotenvx</a>, and <a href="https://railway.com/template/zXEiVF">much</a> <a href="https://forums.docker.com/t/docker-compose-argument-to-replace-env-file-directive-or-argument-to-enable-host-environment-passthrough/141671">more</a>.</li>
  <li><strong>Adoption and Dependents.</strong> Most significantly, major projects adopted dotenvx and became <a href="https://github.com/dotenvx/dotenvx/network/dependents">dotenvx dependents</a>.
    <ul>
      <li><a href="https://github.com/nasa/earthdata-search">NASA</a></li>
      <li><a href="https://docs.amplify.aws/nextjs/deploy-and-host/fullstack-branching/secrets-and-vars/#local-environment-2">AWS</a></li>
      <li><a href="https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md">Supabase</a></li>
      <li><a href="https://github.com/cloudflare/templates">Cloudflare</a></li>
    </ul>
  </li>
</ul>

<p><img src="https://github.com/user-attachments/assets/7cb15832-8e91-494b-b825-4fe0f54fbbed" /></p>

<p>This kind of community involvement makes dotenvx better. The feedback loop between users and development is tight, and that's been key to its fast growth.</p>

<h2 id="ecosystem">Ecosystem</h2>

<p>Dotenvx isn't just a CLI—it's extending into the tools developers use every day.</p>

<ul>
  <li><strong>VS Code Extension.</strong> Decrypt your encrypted .env files in VS Code with the <a href="https://dotenvx.com/vscode-extension/">Dotenvx VS Code Extension</a>.</li>
  <li><strong>Chrome Extension.</strong> Decrypt your encrypted .env files directly on GitHub with the <a href="https://dotenvx.com/chrome-extension">Dotenvx Chrome Extension</a>.</li>
  <li><strong>Buildpacks.</strong> Install dotenvx to Heroku (or any platform that supports buildpacks) with the <a href="https://github.com/dotenvx/heroku-buildpack-dotenvx">Dotenvx Buildpack</a>.</li>
</ul>

<p>This growing ecosystem ensures that no matter where developers work—local editors, browsers, or cloud platforms—dotenvx is increasingly there to seamlessly handle secrets.</p>

<h2 id="standard">Standard</h2>

<p>Dotenvx isn't just widely used—it's built on a foundation that makes it the right way to handle secrets.</p>

<p>From the <a href="https://dotenvx.com/dotenvx.pdf">whitepaper</a>:</p>

<blockquote>
  <p><strong>Dotenvx: Reducing Secrets Risk with Cryptographic Separation</strong></p>

  <p><strong>Abstract.</strong> An ideal secrets solution would not only centralize secrets but also contain the fallout of a breach. While secrets managers offer centralized storage and distribution, their design creates a large blast radius, risking exposure of thousands or even millions of secrets. We propose a solution that reduces the blast radius by splitting secrets management into two distinct components: an encrypted secrets file and a separate decryption key.</p>
</blockquote>

<p>This approach—cryptographic separation—ensures that even if one component is compromised, the overall security of secrets remains intact. The whitepaper breaks down the problem and solution, and "it makes a great case for dotenvx." <sup><a href="https://github.com/dotenvx/dotenvx/issues/537#issue-2881322629">1</a></sup></p>

<iframe src="/dotenvx.pdf?v=20261009021647" class="w-[700px] aspect-[8.5/11] max-w-full max-h-screen border-0 mx-auto"></iframe>

<p>By stating the problem well and implementing good <abbr title="Developer Experience">DX</abbr>, dotenvx is emerging as a new standard for secrets management of .env files. It's pretty incredible.</p>

<h2 id="conclusion">Conclusion</h2>

<p>What started as a rethink of .env files has quickly turned into something much bigger. In just eight months, dotenvx has gone from an emerging idea to an essential tool, trusted by developers and major platforms alike.</p>

<p>With over <strong>1 million monthly installs</strong>, an active and engaged <strong>community</strong>, an expanding <strong>ecosystem</strong>, and a well-defined <strong>standard</strong>, dotenvx isn't just growing—it's reshaping how secrets are managed in modern development.</p>

<p>And this is only the beginning. 💪</p>

<hr />

<p>If you enjoyed this post, please <a href="https://github.com/dotenvx/dotenvx">share dotenvx with friends</a> or <a href="https://github.com/dotenvx/dotenvx">star it on GitHub</a> to help spread the word.</p>]]></content><author><name>Scott Motte</name></author><category term="blog" /><summary type="html"><![CDATA[Eight months ago, dotenvx was just an experiment. Today, it's an essential tool with over 1 million monthly installs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dotenvx.com/assets/img/og/blog-2025-03-03-one-million-installs-html-1c33fd125dcc0be2.png" /><media:content medium="image" url="https://dotenvx.com/assets/img/og/blog-2025-03-03-one-million-installs-html-1c33fd125dcc0be2.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>