<?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: Amit Feldman</title>
    <description>The latest articles on DEV Community by Amit Feldman (@amitfeldman).</description>
    <link>https://dev.to/amitfeldman</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052001%2Fed8ff31d-1d15-45d3-87b2-e461dde0406d.png</url>
      <title>DEV Community: Amit Feldman</title>
      <link>https://dev.to/amitfeldman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amitfeldman"/>
    <language>en</language>
    <item>
      <title>I built a CLI to stop myself from committing API keys (and broken .env files)</title>
      <dc:creator>Amit Feldman</dc:creator>
      <pubDate>Tue, 28 Jul 2026 20:32:15 +0000</pubDate>
      <link>https://dev.to/amitfeldman/i-built-a-cli-to-stop-myself-from-committing-api-keys-and-broken-env-filesi-built-a-cli-to-stop-5eg8</link>
      <guid>https://dev.to/amitfeldman/i-built-a-cli-to-stop-myself-from-committing-api-keys-and-broken-env-filesi-built-a-cli-to-stop-5eg8</guid>
      <description>&lt;h2&gt;
  
  
  The 11pm key rotation
&lt;/h2&gt;

&lt;p&gt;Two things have burned me more than once, and I suspect they've burned you too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incident one:&lt;/strong&gt; a deploy that went green in CI and then crashed on boot in production because &lt;code&gt;.env.production&lt;/code&gt; was missing &lt;code&gt;REDIS_URL&lt;/code&gt;. Nobody caught it, because nothing in the pipeline actually compared the production env file against the keys the app expects. The error message was something unhelpful like &lt;code&gt;undefined is not a function&lt;/code&gt; three stack frames away from the real problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incident two:&lt;/strong&gt; a Stripe test key committed to a repo that was later flipped from private to public. It was a &lt;em&gt;test&lt;/em&gt; key, thankfully, but I still spent an evening rotating it, scrubbing history, and writing the postmortem nobody reads.&lt;/p&gt;

&lt;p&gt;Both of these are embarrassingly common, and both are cheap to catch — you just have to actually check, every build, forever. Humans are bad at "every build, forever." Scripts are good at it. So I wrote one: &lt;strong&gt;envcheck&lt;/strong&gt;, a zero-dependency Node CLI that does two jobs — validating &lt;code&gt;.env&lt;/code&gt; files, and scanning repos for leaked secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job 1: Is my env config sane?
&lt;/h2&gt;

&lt;p&gt;The simplest mode compares an env file against your committed &lt;code&gt;.env.example&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envcheck check .env &lt;span class="nt"&gt;--example&lt;/span&gt; .env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a deliberately broken file, that prints something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.env
  ✗ missing-key      DATABASE_URL is required by .env.example but absent
  ✗ duplicate-key    PORT defined on line 3 and line 9
  ⚠ empty-value      SENTRY_DSN has no value
  ⚠ placeholder-default  API_KEY still equals the example default

1 errors, 2 warnings — 4 keys checked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;placeholder-default&lt;/code&gt; warning is my favorite. It catches the case where someone copied &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and forgot to fill in a real value — so the app boots with &lt;code&gt;API_KEY=your-key-here&lt;/code&gt; and fails somewhere downstream with a confusing auth error.&lt;/p&gt;

&lt;p&gt;When you want more than "same keys as the example," there's a JSON schema format with per-key types, patterns, enums, and length rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowExtra"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"port"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NODE_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"development"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;envcheck check .env.production --schema env.schema.json --strict&lt;/code&gt; fails the build on warnings too. This is the check that would have saved my crashed deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job 2: Am I about to commit a secret?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;envcheck scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This recursively scans the repo for two classes of problems: known token formats (AWS access keys, GitHub and GitLab tokens, Stripe keys, Slack tokens, Google API keys, JWTs, &lt;code&gt;-----BEGIN PRIVATE KEY-----&lt;/code&gt; blocks, and a dozen more), plus generic &lt;code&gt;password = "…"&lt;/code&gt; / &lt;code&gt;api_key: …&lt;/code&gt; assignments filtered by Shannon entropy — which catches random-looking tokens nobody wrote a regex for.&lt;/p&gt;

&lt;p&gt;Output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/config.js:12:15  ✗ github-token   possible GitHub token   ghp_…89 (40 chars)
deploy/notes.txt:4   ✗ high-entropy   high-entropy string     7Kd2…Qz (36 chars)
.env                 ✗ env-file       .env file present in scanned tree

2 errors — 42 files scanned, 3 skipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design decisions that matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matches are redacted.&lt;/strong&gt; Reports are safe to archive as CI artifacts; envcheck never prints your full secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise control is built in.&lt;/strong&gt; &lt;code&gt;node_modules&lt;/code&gt;, lockfiles, minified files, source maps, and binaries are skipped automatically, and placeholder values like &lt;code&gt;your-key-here&lt;/code&gt; don't cry wolf. A scanner that floods you with false positives gets disabled within a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes are boring on purpose.&lt;/strong&gt; 0 = clean, 1 = findings, 2 = usage error. Every CI system on earth understands that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What CI integration looks like
&lt;/h2&gt;

&lt;p&gt;GitHub Actions, in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;22&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx envcheck check .env.ci --example .env.example --strict&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx envcheck scan . --json &amp;gt; envcheck-report.json&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. Zero runtime dependencies, nothing to configure, works offline, Node 18+. It also exposes &lt;code&gt;--json&lt;/code&gt; on every command if you want to feed results into your own tooling, and a programmatic API (&lt;code&gt;parseEnv&lt;/code&gt;, &lt;code&gt;validateEnv&lt;/code&gt;, &lt;code&gt;scanContent&lt;/code&gt;) if you want to embed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a scanner, not a vault.&lt;/strong&gt; It finds secrets in files; it doesn't manage them. If you need secret storage, that's Vault/Doppler/1Password territory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entropy detection has a floor.&lt;/strong&gt; Short random tokens (under ~20 chars) often don't have enough entropy to distinguish from normal identifiers. The known-format regexes cover the common cases; genuinely obscure short tokens can slip through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It won't catch a secret that's already in git history&lt;/strong&gt; — it scans working-tree files, not &lt;code&gt;git log&lt;/code&gt;. If a key was committed and deleted, you still need to rotate it. (Rotate it. Scrubbing history is not enough.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-only runtime.&lt;/strong&gt; The validation logic is plain JS with no dependencies, so porting is feasible, but today you need Node 18+.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting it
&lt;/h2&gt;

&lt;p&gt;envcheck is $12, one-time, on Gumroad: &lt;strong&gt;&lt;a href="https://afeldman2.gumroad.com/l/yxtnc?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=envcheck-launch" rel="noopener noreferrer"&gt;https://afeldman2.gumroad.com/l/yxtnc?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=envcheck-launch&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That gets you the complete MIT-licensed source (audit every line — it's yours), the schema format docs, example files including a deliberately-broken env file for demos, and the full 65-test suite so you can verify it does what this post claims. Cheaper than one evening of rotating a leaked key.&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
  </channel>
</rss>
