<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harsh Sonkar</title>
    <description>The latest articles on DEV Community by Harsh Sonkar (@harshsonkar).</description>
    <link>https://dev.to/harshsonkar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1351170%2F4326215b-11bf-4c2b-acb1-a45accdafd78.jpeg</url>
      <title>DEV Community: Harsh Sonkar</title>
      <link>https://dev.to/harshsonkar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshsonkar"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Using .env Files (And Built a Zero-Disk Go CLI Instead)</title>
      <dc:creator>Harsh Sonkar</dc:creator>
      <pubDate>Wed, 03 Jun 2026 14:58:10 +0000</pubDate>
      <link>https://dev.to/harshsonkar/why-i-stopped-using-env-files-and-built-a-zero-disk-go-cli-instead-2e00</link>
      <guid>https://dev.to/harshsonkar/why-i-stopped-using-env-files-and-built-a-zero-disk-go-cli-instead-2e00</guid>
      <description>&lt;p&gt;You know the freeze. &lt;code&gt;git push&lt;/code&gt;, then half a second later: &lt;em&gt;wait — is &lt;code&gt;.env&lt;/code&gt; actually in &lt;code&gt;.gitignore&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file is how most of us handle secrets locally, and it's a mess. Plaintext. Sits on laptops forever. Gets pasted into Slack. Occasionally rides along into a public commit. Even teams paying for a proper secrets manager tend to do the same local dance: open the vault UI, copy the token, paste it into a &lt;code&gt;.env&lt;/code&gt;, pray. I got tired of that, so I tried to get rid of the file entirely.&lt;/p&gt;

&lt;p&gt;Here's the thing that bugs me about the status quo: production and local are worlds apart. Vault or a cloud SDK on a server? Fine, reasonable. But making someone run a daemon or write SDK boilerplate just to read a DB string on their own laptop is friction, and people route around friction every time. That's how plaintext creds end up on a dozen machines in the first place.&lt;/p&gt;

&lt;p&gt;So I gave myself three rules: nothing plaintext on disk, no changes to app code (it has to work with Node, Python, Go, Rust, whatever), and no daemon running in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;env-pull&lt;/code&gt; is a small Go CLI. Rather than write secrets to a file, it wraps your command, pulls the secrets at runtime, and hands them straight to the child process's environment. Here's the path when it's pulling from AWS Secrets Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;env-pull run --aws-secret prod/db -- psql mydb
  │
  ├─ 1. Load AWS default credential chain (zero new config)
  ├─ 2. Call secretsmanager:GetSecretValue("prod/db")
  ├─ 3. Parse JSON payload → map[string]string in memory
  ├─ 4. Merge with current environment (no overwrites)
  ├─ 5. exec(psql, env=[...existing..., DB_PASSWORD=s3cr3t, ...])
  └─ 6. psql exits → memory is reclaimed, secrets are gone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's plain &lt;code&gt;os/exec&lt;/code&gt; underneath, so your app just sees normal environment variables — no idea anything injected them, no code to change. The moment the process exits, the OS takes the memory back and the secrets go with it. Nothing to clean up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;Here's a throwaway Node script that just prints what it finds in the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.js&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=== Application Lifecycle Initialized ===&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Database Access String : &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[MISSING - Connection Failed]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment Gateway Token  : &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[MISSING - Fallback Triggered]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=== Application Lifecycle Terminated ===&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it the normal way and there's nothing there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;node index.js
&lt;span class="o"&gt;===&lt;/span&gt; Application Lifecycle Initialized &lt;span class="o"&gt;===&lt;/span&gt;
Database Access String :  &lt;span class="o"&gt;[&lt;/span&gt;MISSING - Connection Failed]
Payment Gateway Token  :  &lt;span class="o"&gt;[&lt;/span&gt;MISSING - Fallback Triggered]
&lt;span class="o"&gt;===&lt;/span&gt; Application Lifecycle Terminated &lt;span class="o"&gt;===&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The local encrypted flow
&lt;/h2&gt;

&lt;p&gt;If you're not on a cloud provider, there's an offline path. &lt;code&gt;env-pull edit&lt;/code&gt; opens your default editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;env-pull edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You type variables like a normal &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgres://local_dev_user:crypt_pass_99@localhost:5432/dev_db&lt;/span&gt;
&lt;span class="py"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk_test_51NxLocalOverrideKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On save, &lt;code&gt;env-pull&lt;/code&gt; grabs the buffer, overwrites the temp file with zeros, and encrypts the payload with AES-256-GCM. What's on disk is ciphertext and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .env.pull.enc
U2FsdGVkX19vPlu8Y1bqm1h3S6Kx8vW9ZJ2H0m9L7qX+P... &lt;span class="o"&gt;[&lt;/span&gt;Encrypted Binary Stream]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run, prefix your command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;env-pull run &lt;span class="nt"&gt;--&lt;/span&gt; node index.js
&lt;span class="o"&gt;===&lt;/span&gt; Application Lifecycle Initialized &lt;span class="o"&gt;===&lt;/span&gt;
Database Access String :  postgres://local_dev_user:crypt_pass_99@localhost:5432/dev_db
Payment Gateway Token  :  sk_test_51NxLocalOverrideKey
&lt;span class="o"&gt;===&lt;/span&gt; Application Lifecycle Terminated &lt;span class="o"&gt;===&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the app the normal way right afterward and the values are already gone — not on disk, not in your shell history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;node index.js
&lt;span class="o"&gt;===&lt;/span&gt; Application Lifecycle Initialized &lt;span class="o"&gt;===&lt;/span&gt;
Database Access String :  &lt;span class="o"&gt;[&lt;/span&gt;MISSING - Connection Failed]
&lt;span class="o"&gt;===&lt;/span&gt; Application Lifecycle Terminated &lt;span class="o"&gt;===&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this helps, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Now the honest part, because there's no silver bullet here. &lt;code&gt;env-pull&lt;/code&gt; is built for software-layer leaks: accidental commits, screen shares, plaintext sitting at rest on an SSD. It is not a hardware guarantee.&lt;/p&gt;

&lt;p&gt;Zeroing buffers in a GC'd language like Go is best-effort, full stop. On copy-on-write or log-structured filesystems — APFS, ZFS, most NVMe SSDs — the OS shuffles physical blocks around on its own, so if you want a true physical wipe you need block-level disk encryption underneath. And anyone with root can read &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/environ&lt;/code&gt; or dump memory; if your machine is fully owned, this won't save you. What it does kill is the boring, everyday leak that actually takes teams down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I built this for my own workflow, but I'm wiring in more upstream integrations: GCP Secret Manager, Azure Key Vault, 1Password CLI, and Bitwarden CLI are in progress.&lt;/p&gt;

&lt;p&gt;It's open source, and I'd really like people poking at it — read the code, try the install, tell me what I got wrong. Repo's here. If a Homebrew or Scoop install chokes, open an issue.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/dynamicHarsh/env-pull" rel="noopener noreferrer"&gt;https://github.com/dynamicHarsh/env-pull&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>go</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
