<?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: benjamin</title>
    <description>The latest articles on DEV Community by benjamin (@_06a3df6b50aec966668fb).</description>
    <link>https://dev.to/_06a3df6b50aec966668fb</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%2F3975620%2Fbec1cbbc-f5ef-4807-b03b-92d5228e33a4.png</url>
      <title>DEV Community: benjamin</title>
      <link>https://dev.to/_06a3df6b50aec966668fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_06a3df6b50aec966668fb"/>
    <language>en</language>
    <item>
      <title>I built a local dead-man's-switch for cron jobs (no server, no signup)</title>
      <dc:creator>benjamin</dc:creator>
      <pubDate>Tue, 09 Jun 2026 16:08:39 +0000</pubDate>
      <link>https://dev.to/_06a3df6b50aec966668fb/i-built-a-local-dead-mans-switch-for-cron-jobs-no-server-no-signup-3j1m</link>
      <guid>https://dev.to/_06a3df6b50aec966668fb/i-built-a-local-dead-mans-switch-for-cron-jobs-no-server-no-signup-3j1m</guid>
      <description>&lt;p&gt;Cron has a cruel design flaw: it tells you nothing when a job &lt;em&gt;stops&lt;/em&gt; running.&lt;/p&gt;

&lt;p&gt;A job that exits non-zero? Silent. A job that never fires because the host was&lt;br&gt;
asleep? Silent. A backup script that's been dying at 2 a.m. for three days&lt;br&gt;
straight? Completely silent — right up until the morning you actually need that&lt;br&gt;
backup and discover the last good copy is from Tuesday.&lt;/p&gt;

&lt;p&gt;The failure mode isn't "the job errored." It's &lt;strong&gt;the absence of success&lt;/strong&gt;, and&lt;br&gt;
absence is exactly the thing a logs-and-exit-codes mental model can't catch.&lt;/p&gt;
&lt;h2&gt;
  
  
  The usual fix, and why it bugged me
&lt;/h2&gt;

&lt;p&gt;The classic answer is a heartbeat monitor: your job pings a URL when it finishes,&lt;br&gt;
and a watchdog yells if the ping doesn't arrive on time. healthchecks.io,&lt;br&gt;
Cronitor, Dead Man's Snitch — all good, all battle-tested.&lt;/p&gt;

&lt;p&gt;But for a box of personal cron jobs and a few self-hosted boxes, they felt like&lt;br&gt;
overkill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I had to create an account.&lt;/li&gt;
&lt;li&gt;My job names and timing metadata went to a third party.&lt;/li&gt;
&lt;li&gt;It was one more SaaS dashboard to log into and forget about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the &lt;em&gt;heartbeat&lt;/em&gt; idea without the &lt;em&gt;hosted&lt;/em&gt; part. So I built &lt;strong&gt;deadcron&lt;/strong&gt; —&lt;br&gt;
a single CLI, zero dependencies, no server, no signup. State lives in a JSON file&lt;br&gt;
under &lt;code&gt;~/.deadcron&lt;/code&gt;. Nothing leaves your machine unless you explicitly point it at&lt;br&gt;
a webhook or SMTP server.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Three moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Each run checks in.&lt;/strong&gt; Wrap your command and deadcron records a "ping" on
exit 0 (and the exit code on failure):
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# before:&lt;/span&gt;
   0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  /opt/backup.sh

   &lt;span class="c"&gt;# after:&lt;/span&gt;
   0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  deadcron run backup &lt;span class="nt"&gt;--every&lt;/span&gt; 1d &lt;span class="nt"&gt;--grace&lt;/span&gt; 1h &lt;span class="nt"&gt;--&lt;/span&gt; /opt/backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A job declares its expected rhythm&lt;/strong&gt; with &lt;code&gt;--every&lt;/code&gt; (plus an optional&lt;br&gt;
&lt;code&gt;--grace&lt;/code&gt;). If the time since the last successful check-in exceeds&lt;br&gt;
&lt;code&gt;every + grace&lt;/code&gt;, the job is &lt;strong&gt;overdue&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A watchdog runs &lt;code&gt;check&lt;/code&gt;&lt;/strong&gt; on its own schedule. You add it to cron once:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   deadcron &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--every&lt;/span&gt; 5m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. Now if &lt;code&gt;backup&lt;/code&gt; doesn't succeed within a day (+1h slack), the next&lt;br&gt;
&lt;code&gt;check&lt;/code&gt; tick notices the silence and alerts you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deadcron status
&lt;span class="go"&gt;● backup    ok                every 1d   last: 3h ago
● sync      OVERDUE by 2h     every 1h   last: 5h ago
● report    FAILED (exit 1)   every 1w   last: 2d ago
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; exits non-zero when anything is overdue or failed, so it also drops&lt;br&gt;
straight into CI or any other scheduler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deadcron check            &lt;span class="c"&gt;# exit 1 if unhealthy&lt;/span&gt;
deadcron check &lt;span class="nt"&gt;--json&lt;/span&gt;     &lt;span class="c"&gt;# machine-readable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Alerts, but only where you want them
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;check&lt;/code&gt; fires every channel you've turned on — and they all run locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deadcron config &lt;span class="nb"&gt;enable &lt;/span&gt;macos                 &lt;span class="c"&gt;# native notification&lt;/span&gt;
deadcron config set-webhook https://hooks.slack.com/services/XXX
deadcron config set-email &lt;span class="nt"&gt;--to&lt;/span&gt; me@you.com &lt;span class="nt"&gt;--sendmail&lt;/span&gt;
deadcron config &lt;span class="nb"&gt;test&lt;/span&gt;                         &lt;span class="c"&gt;# send a sample through all of them&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;terminal&lt;/td&gt;
&lt;td&gt;writes to stderr (great for CI / piping)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;native banner via &lt;code&gt;osascript&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webhook&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST&lt;/code&gt; JSON &lt;code&gt;{event, checkedAt, jobs}&lt;/code&gt; → Slack/Discord/your endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;local &lt;code&gt;sendmail&lt;/code&gt;, or direct SMTP (TLS 465 / STARTTLS, optional auth)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Repeat alerts are throttled (default: once per hour per job) so a long outage&lt;br&gt;
doesn't turn into a notification flood.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;It ships on both registries, because half my cron jobs are Node and half are&lt;br&gt;
Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx deadcron            &lt;span class="c"&gt;# Node — zero deps&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;deadcron    &lt;span class="c"&gt;# Python — pure stdlib&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both implementations share the exact same &lt;code&gt;~/.deadcron&lt;/code&gt; state format, so you can&lt;br&gt;
mix them on one machine and they won't step on each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few design notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No daemon.&lt;/strong&gt; deadcron isn't a long-running process — it's just a CLI plus a
state file. The "watchdog" is literally &lt;code&gt;deadcron check&lt;/code&gt; invoked by your own
cron. Less to crash, less to babysit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run&lt;/code&gt; over &lt;code&gt;ping&lt;/code&gt;.&lt;/strong&gt; You &lt;em&gt;can&lt;/em&gt; call &lt;code&gt;deadcron ping &amp;lt;name&amp;gt;&lt;/code&gt; at the end of a
script, but wrapping with &lt;code&gt;run&lt;/code&gt; also captures non-zero exit codes, so a job
that fires on time but crashes is still flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;grace&lt;/code&gt; matters.&lt;/strong&gt; A job that runs "every hour" never runs at exactly one
hour. Grace is the slack that keeps borderline timing from crying wolf.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it / break it
&lt;/h2&gt;

&lt;p&gt;Code, issues, and the full README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node: &lt;a href="https://github.com/jjdoor/deadcron" rel="noopener noreferrer"&gt;https://github.com/jjdoor/deadcron&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python: &lt;a href="https://github.com/jjdoor/deadcron-py" rel="noopener noreferrer"&gt;https://github.com/jjdoor/deadcron-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT, it's tiny, and I'd genuinely like to know where it falls over. If&lt;br&gt;
you've got a cron job you'd be sad to lose silently, point deadcron at it and&lt;br&gt;
tell me what's missing.&lt;/p&gt;

&lt;p&gt;What do you use today to know your scheduled jobs are &lt;em&gt;actually&lt;/em&gt; running?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>devops</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a 1-file CLI to catch .env drift before it causes production bugs</title>
      <dc:creator>benjamin</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:00:48 +0000</pubDate>
      <link>https://dev.to/_06a3df6b50aec966668fb/i-built-a-1-file-cli-to-catch-env-drift-before-it-causes-production-bugs-3735</link>
      <guid>https://dev.to/_06a3df6b50aec966668fb/i-built-a-1-file-cli-to-catch-env-drift-before-it-causes-production-bugs-3735</guid>
      <description>&lt;p&gt;Almost every developer has fought &lt;code&gt;.env&lt;/code&gt; issues. They're silent, annoying, and they only show up &lt;em&gt;after&lt;/em&gt; you deploy or &lt;em&gt;after&lt;/em&gt; someone clones the repo.&lt;/p&gt;

&lt;p&gt;Here's the exact failure I hit one too many times:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I add &lt;code&gt;STRIPE_KEY&lt;/code&gt; to my local &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I ship code that reads &lt;code&gt;process.env.STRIPE_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I forget to add &lt;code&gt;STRIPE_KEY&lt;/code&gt; to the committed &lt;code&gt;.env.example&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A teammate clones, runs the app, and it crashes on a missing variable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reverse is just as common: &lt;code&gt;.env.example&lt;/code&gt; promises a key you never actually set locally — a silent misconfig waiting to bite.&lt;/p&gt;

&lt;p&gt;The two files are &lt;em&gt;supposed&lt;/em&gt; to declare the same set of keys. They drift apart constantly. So I built a small guardrail.&lt;/p&gt;

&lt;h2&gt;
  
  
  dotdrift
&lt;/h2&gt;

&lt;p&gt;Zero dependencies, one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx dotdrift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x drift detected between .env and .env.example

  Missing in .env.example (add these so teammates know they're needed):
    + STRIPE_KEY

  Tip: run "dotdrift sync" to update .env.example automatically.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits non-zero on drift, so it drops straight into CI:&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx dotdrift --strict&lt;/span&gt;   &lt;span class="c1"&gt;# --strict also flags empty values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part I actually wanted: &lt;code&gt;sync&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Detecting drift is half the problem. The annoying half is &lt;em&gt;fixing&lt;/em&gt; the template by hand. So &lt;code&gt;sync&lt;/code&gt; regenerates &lt;code&gt;.env.example&lt;/code&gt; from your real &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 shell"&gt;&lt;code&gt;npx dotdrift &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;values are stripped (&lt;code&gt;KEY=&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;your comments and blank-line grouping are preserved,&lt;/li&gt;
&lt;li&gt;curated placeholders already in the template survive (a hand-written &lt;code&gt;PORT=3000&lt;/code&gt; stays),&lt;/li&gt;
&lt;li&gt;it's idempotent — run it twice, nothing changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So updating the template is now one command instead of a manual chore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it permanent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx dotdrift hook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installs a &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; that blocks commits when things drift. A one-time tool becomes a continuous safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monorepo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx dotdrift &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scans every subdirectory that has an &lt;code&gt;.env&lt;/code&gt;/&lt;code&gt;.env.example&lt;/code&gt; pair (skips &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, etc.) and reports per service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Zero dependencies — Node stdlib only, ~250 lines.&lt;/li&gt;
&lt;li&gt;It only ever compares &lt;strong&gt;key names&lt;/strong&gt; (and, with &lt;code&gt;--strict&lt;/code&gt;, whether a value is empty). It never prints or transmits your secret values.&lt;/li&gt;
&lt;li&gt;Understands &lt;code&gt;KEY=value&lt;/code&gt;, &lt;code&gt;export KEY=value&lt;/code&gt;, single/double quotes, &lt;code&gt;KEY=&lt;/code&gt; (empty), &lt;code&gt;#&lt;/code&gt; comments, and inline comments on unquoted values.&lt;/li&gt;
&lt;li&gt;MIT licensed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/jjdoor/dotdrift" rel="noopener noreferrer"&gt;https://github.com/jjdoor/dotdrift&lt;/a&gt;&lt;br&gt;
npm: &lt;code&gt;npx dotdrift&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you hit a parsing edge case or the &lt;code&gt;sync&lt;/code&gt; output isn't what you'd expect, open an issue — that's exactly the feedback I'm after.&lt;/p&gt;

</description>
      <category>node</category>
      <category>cli</category>
      <category>devtools</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
