<?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: Pinnadash</title>
    <description>The latest articles on DEV Community by Pinnadash (@pinnadash).</description>
    <link>https://dev.to/pinnadash</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%2F4145806%2F95bab0bf-ec91-4b08-ac9e-0864b2538031.png</url>
      <title>DEV Community: Pinnadash</title>
      <link>https://dev.to/pinnadash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pinnadash"/>
    <language>en</language>
    <item>
      <title>When curl prints 000: building an egress check that cannot lie</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:40:47 +0000</pubDate>
      <link>https://dev.to/pinnadash/when-curl-prints-000-building-an-egress-check-that-cannot-lie-i27</link>
      <guid>https://dev.to/pinnadash/when-curl-prints-000-building-an-egress-check-that-cannot-lie-i27</guid>
      <description>&lt;h2&gt;
  
  
  The alarm
&lt;/h2&gt;

&lt;p&gt;Our daily ops job has one job: decide whether this machine can reach the public internet. It ran a plain &lt;code&gt;curl&lt;/code&gt; against an overseas endpoint, got &lt;code&gt;000&lt;/code&gt;, and marked the outbound link down. The publish queue stopped. Nothing else in the stack complained.&lt;/p&gt;

&lt;p&gt;Then the real egress check ran — the one that goes through the proxy the same way production traffic does — and reported &lt;code&gt;up (HTTP 204, TLS verified)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both tools were "measuring the network". They disagreed, so at least one of them was lying. It was the curl.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;000&lt;/code&gt; is not a status code. It is the absence of one.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl -w '%{http_code}'&lt;/code&gt; prints three digits because the format string is three digits wide, not because it has an HTTP status to give you. &lt;code&gt;000&lt;/code&gt; means "no HTTP response was received". DNS failure, connection refused, TLS handshake failure, timeout — all of them collapse into the same &lt;code&gt;000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which makes it a falsy integer that looks like data, and exactly the value that an &lt;code&gt;if code != 200&lt;/code&gt; check will silently convert into "the world is down".&lt;/p&gt;

&lt;p&gt;The cause in our case: curl reads &lt;code&gt;http_proxy&lt;/code&gt; / &lt;code&gt;https_proxy&lt;/code&gt; from the environment, but it does not read the &lt;em&gt;OS&lt;/em&gt; proxy settings — not the Windows WinINET/WinHTTP configuration, not an application's own "system proxy" toggle. The proxy was configured in a GUI. The environment variable was never exported into that subprocess. So curl honestly reported "I was told to connect directly, and direct is blackholed." The process was right. The inference was wrong.&lt;/p&gt;

&lt;p&gt;Node's built-in &lt;code&gt;fetch&lt;/code&gt; (undici) has the same trap: no system proxy, no &lt;code&gt;HTTP_PROXY&lt;/code&gt; pickup. Without an explicit &lt;code&gt;ProxyAgent&lt;/code&gt; the request goes direct and dies with an &lt;code&gt;ECONNREFUSED&lt;/code&gt; that reads exactly like an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1 — probe the path production actually uses
&lt;/h2&gt;

&lt;p&gt;If your application's traffic goes through a proxy, the health check must go through the proxy. "Can this box reach the internet directly?" is a different question, and usually the answer doesn't matter.&lt;/p&gt;

&lt;p&gt;Our probe became two stages, each with its own verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Stage 1: is the proxy even listening?
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;tcp_alive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy port refused&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# Stage 2: can we get an HTTP response through it?
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;proxy_opener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.google.com/generate_204&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcode&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# 4xx/5xx: TLS path worked, the remote answered
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;URLError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter more than the code itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The stage is named in the output. "Proxy not running" and "proxy up, node dead" produce the same red light but completely different phone calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any HTTP answer below 500 counts as a live path.&lt;/strong&gt; A &lt;code&gt;403&lt;/code&gt; from a remote host proves DNS, CONNECT and TLS all worked. The remote refusing &lt;em&gt;you&lt;/em&gt; is not the network being down. Only "no response at all" is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule 2 — give "unknown" its own state, and its own exit code
&lt;/h2&gt;

&lt;p&gt;The original script had two outcomes: up and down. Anything that wasn't a 200 became "down" — including the script crashing. A typo in the check reported the network as broken, and a human spent an hour on proxy settings because of a &lt;code&gt;NameError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So: three codes, plus a rule that a bug may never look like an outage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;code&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;path verified up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;path verified down (port refused, or no response at all)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;state could not be persisted / internal error — the reading is untrustworthy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second half of the same idea lives in the dashboard. The state file stores &lt;code&gt;ok&lt;/code&gt; plus &lt;code&gt;last_ok_at&lt;/code&gt;, and the renderer maps &lt;code&gt;ok: null&lt;/code&gt; to &lt;strong&gt;"not probed yet"&lt;/strong&gt; — never to "down". The most valuable line in the whole file is this one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I didn't measure" is not "there was no data."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Uptime dashboards get this wrong constantly, and then a panel shows &lt;code&gt;0%&lt;/code&gt; for a host that was simply never scraped. &lt;code&gt;0%&lt;/code&gt; gets escalated; "no data" gets investigated by whoever owns the scrape. Different problems, different words.&lt;/p&gt;

&lt;p&gt;The corollary for red days: keep &lt;code&gt;last_ok_at&lt;/code&gt; and never overwrite it on failure. &lt;code&gt;down; last success = 2026-10-24 09:12&lt;/code&gt; tells you how long you've been out. A bare &lt;code&gt;down&lt;/code&gt; restarts the clock every run, and you can no longer tell a five-minute blip from a three-day outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3 — exactly one function writes the human sentence
&lt;/h2&gt;

&lt;p&gt;The probe writes the status string; the dashboard, the daily report and the alert channel all call that same function. As soon as a consumer composes its own phrasing, they drift — and on the day they drift you get a green panel beside a red alarm with two different timestamps, and nobody believes either.&lt;/p&gt;

&lt;p&gt;Ours has four branches: not probed / up / down with history / down without history. Every branch is a sentence a human can act on, and "whose hand fixes this" is written into the string — because the fix for a dead proxy is a person opening a GUI on their desktop, and that is not something a monitoring job can do to itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4 — a 200 can still mean "gone"
&lt;/h2&gt;

&lt;p&gt;While we were in there: a vendor's submission page answered &lt;code&gt;200&lt;/code&gt; with a 39-byte empty document. Both of our tools called it "available". It wasn't — the page was a skeleton and the form had been removed. Two others in the same list answered 404, one refused to connect, and sorting by status code would have ranked them exactly backwards.&lt;/p&gt;

&lt;p&gt;A pass state has to be producible &lt;em&gt;only&lt;/em&gt; by the thing you actually care about. For "is the outbound path alive?" the only acceptable green is a real HTTP response through the real proxy. For "does the submission form still exist?" the answer is the form in the DOM, not a status line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a false red is worse than no signal
&lt;/h2&gt;

&lt;p&gt;A missed alert costs you one incident. A false alarm costs you every future alert: people learn that the red light means "go look at the box, it's probably nothing", and the real outage then queues up behind it. Our &lt;code&gt;000&lt;/code&gt; didn't just stall one publish — it taught everyone a little more distrust of the check.&lt;/p&gt;

&lt;p&gt;So the check must refuse to say "down" until it can prove that the path it tested is the path that matters. Everything above is one question asked four times: &lt;strong&gt;what else could produce this same result?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Write your probe so that its green is impossible to fake and its red is impossible to misread.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you build dev tools, come get a key
&lt;/h2&gt;

&lt;p&gt;We make small desktop utilities — batch image processing, a local-first disk manager, and a CLI — at &lt;a href="https://pinnadash.com" rel="noopener noreferrer"&gt;pinnadash.com&lt;/a&gt;. Early users get &lt;strong&gt;3 free months of membership&lt;/strong&gt;: no card, no trial hoops, just a redeem key. Comment below or DM me here on dev.to and I'll send you one.&lt;/p&gt;

&lt;p&gt;And if you've been burned by a lying health check, tell me which one — I'm collecting them.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devtools</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Your health check is lying: three ways a green pipeline stays broken</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Sun, 25 Oct 2026 08:44:13 +0000</pubDate>
      <link>https://dev.to/pinnadash/your-health-check-is-lying-three-ways-a-green-pipeline-stays-broken-35f</link>
      <guid>https://dev.to/pinnadash/your-health-check-is-lying-three-ways-a-green-pipeline-stays-broken-35f</guid>
      <description>&lt;p&gt;A scraper pipeline I maintain stopped publishing for five weeks. Every morning the report said the same thing: &lt;strong&gt;logged in, ready to capture&lt;/strong&gt;. Nothing was captured.&lt;/p&gt;

&lt;p&gt;It was not broken in any dramatic way. It was broken the way most automation breaks: a check answered a question slightly different from the one that mattered, and the answer was always yes.&lt;/p&gt;

&lt;p&gt;Here are the three shapes I found, and the probe rules I now apply everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Logged in" measured the presence of a file
&lt;/h2&gt;

&lt;p&gt;The check ran &lt;code&gt;auth status --offline&lt;/code&gt;. That flag does exactly what it says: read the credential file, make no network request. The session behind it had been rejected with a 401 nineteen days earlier. The file was still there, so the verdict was "logged in, you can capture now".&lt;/p&gt;

&lt;p&gt;With a cache in front of reality, &lt;em&gt;liveness&lt;/em&gt; and &lt;em&gt;existence&lt;/em&gt; become different questions. An offline check is a cache check.&lt;/p&gt;

&lt;p&gt;The replacement: exactly one request to an identity endpoint, judged by status code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/v4/me
  200 -&amp;gt; session is real; print the username
  401 -&amp;gt; "credentials expired"; print the re-auth instruction
  5xx -&amp;gt; "unverified"; never green
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three outcomes, never two. A check that can only say green or red will invent a green the moment the network hiccups.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A moved environment broke a path, and the break was silent
&lt;/h2&gt;

&lt;p&gt;The CLI package lived on the C: drive. A toolchain migration moved the package root to D: and set an environment variable pointing at it. Our wrapper still resolved the old absolute path.&lt;/p&gt;

&lt;p&gt;What kept anyone from noticing is the interesting part: the wrapper caught the launch error and logged it at debug level, so the daily job reported a successful run that produced zero items. Nothing failed. Nothing worked.&lt;/p&gt;

&lt;p&gt;Two changes fixed it. Resolve the path from the environment variable, documenting the legacy default as a fallback. And make "binary missing" a hard failure instead of a zero-row success. Zero rows is a result; a missing binary is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The gate was measuring the instrument, not the channel
&lt;/h2&gt;

&lt;p&gt;Our channel gate called &lt;code&gt;fetch('https://dev.to/api/users/me')&lt;/code&gt; and the call failed. The credential was fine. The API was fine. The host simply does not resolve from this network without the local proxy — and Node's built-in fetch ignores system proxy settings unless you wire a dispatcher. A one-line &lt;code&gt;curl -x&lt;/code&gt; proved reachability in under a second.&lt;/p&gt;

&lt;p&gt;So the gate printed "API channel failed": an accurate observation about the instrument and a wrong conclusion about the world. Run on a schedule, it would have blocked every publish for a month, honestly.&lt;/p&gt;

&lt;p&gt;The rule now: a probe must name the layer it failed at. DNS, TCP, TLS, HTTP, auth, parse — six boxes, and only the ones that actually answered get printed. "Failed" is not a diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: probes need positive evidence
&lt;/h2&gt;

&lt;p&gt;All three bugs are one bug: a success criterion that can be satisfied without doing the thing it claims to verify.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;presence of a credential is not a valid session&lt;/li&gt;
&lt;li&gt;a zero-row success is not a working pipeline&lt;/li&gt;
&lt;li&gt;a reachability failure is not a channel failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The probe rules that survived:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every probe touches reality: one real request or one real DOM read per claim.&lt;/li&gt;
&lt;li&gt;Verdicts are three-valued: pass, fail-with-reason, unverified. Unverified never renders as pass.&lt;/li&gt;
&lt;li&gt;A probe reports the layer: what answered and what did not.&lt;/li&gt;
&lt;li&gt;Failures name the next action: which human, which window, which click.&lt;/li&gt;
&lt;li&gt;Store the smallest evidence that proves the verdict — a username, a status code, a byte count — not the whole response body.&lt;/li&gt;
&lt;li&gt;When a write depends on a probe, the write stays blocked until the probe passes. "Materials ready" is not "executable".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic. It is the difference between asking "does the file exist" and asking "do I actually work right now".&lt;/p&gt;

&lt;p&gt;A pipeline that reports green for five weeks is more dangerous than one that crashes on day one. The crash gets fixed the same morning; the green one gets trusted.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>automation</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>An inbox your AI agent can read: Cloudflare Email Routing -&gt; Worker -&gt; D1, plus the webmail we added this week</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Tue, 20 Oct 2026 02:20:29 +0000</pubDate>
      <link>https://dev.to/pinnadash/an-inbox-your-ai-agent-can-read-cloudflare-email-routing-worker-d1-plus-the-webmail-we-2k9g</link>
      <guid>https://dev.to/pinnadash/an-inbox-your-ai-agent-can-read-cloudflare-email-routing-worker-d1-plus-the-webmail-we-2k9g</guid>
      <description>&lt;p&gt;I run a one-person software company where AI agents do most of the work. That has one surprisingly annoying prerequisite: &lt;strong&gt;the agents need to receive email.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Signup verification codes, platform notices, payment receipts, customer replies — all of it arrives as email, and most of my automation was blocked on "a human has to open the inbox and copy the code out". IMAP polling from a script works, but it is flaky, stateful, and every agent needs its own credentials.&lt;/p&gt;

&lt;p&gt;So I built an internal mail hub. This week it grew a webmail client, and that turned out to be the interesting part, because the whole thing runs on Cloudflare's free tier. Here is the architecture, the decisions that mattered, and the four pitfalls worth stealing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MX (Cloudflare Email Routing)
   └─&amp;gt; Email Worker            one entry point for every address on the domain
         ├─ classify           verification / platform / spam / human
         └─ D1                 every message persisted = durable queue
                ├─ CLI         the agent-facing interface (drain, wait, cred, triage…)
                └─ daemon      callback per category
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties did the heavy lifting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. D1 is the queue, not memory.&lt;/strong&gt; The Worker writes the message, its classification, and its processing state into D1 before doing anything else. If my laptop is off, if the agent crashes mid-run, if I redeploy the Worker — the mail is still there. Every later step (CLI drain, daemon callback, done/ignore marking) is a state transition on a row, not an event that can be missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classification at ingest, not at read time.&lt;/strong&gt; The Worker tags each message as &lt;code&gt;verification&lt;/code&gt;, &lt;code&gt;platform&lt;/code&gt;, &lt;code&gt;spam&lt;/code&gt;, or &lt;code&gt;human&lt;/code&gt; before it is queued. That single decision is what makes the CLI usable: an agent asking for a login code never has to read marketing mail, and a spam sweep never touches a genuine customer reply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A CLI is the interface an agent actually wants.&lt;/strong&gt; Twenty commands, deterministic exit codes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;easy-mail drain              &lt;span class="c"&gt;# what arrived since the last session&lt;/span&gt;
easy-mail &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 60  &lt;span class="c"&gt;# block until a verification mail lands&lt;/span&gt;
easy-mail cred &amp;lt;selector&amp;gt;    &lt;span class="c"&gt;# extract the code / signed link from it&lt;/span&gt;
easy-mail triage &lt;span class="nt"&gt;--apply&lt;/span&gt;     &lt;span class="c"&gt;# apply the rule set to the queue&lt;/span&gt;
easy-mail &lt;span class="k"&gt;done &lt;/span&gt;12 15 19      &lt;span class="c"&gt;# mark processed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tool call that returns "0 new messages, exit 0" is worth far more to an agent than an API with pagination and a half-page of JSON. Exit codes are the contract: &lt;code&gt;0&lt;/code&gt; nothing to do / done, &lt;code&gt;1&lt;/code&gt; needs attention, &lt;code&gt;2&lt;/code&gt; usage error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we added this week: a webmail client on the free tier
&lt;/h2&gt;

&lt;p&gt;Until now the hub was agent-only. I still had to open a mail client to read the human mail. So the hub got a web client — Pages for the SPA, a Worker for the API, D1 for storage. No paid Workers plan, no Access seat.&lt;/p&gt;

&lt;p&gt;The access model is deliberately boring and deliberately &lt;em&gt;not&lt;/em&gt; single-sign-on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One mailbox = one account + one password.&lt;/strong&gt; Not one login that can see every address. Each person gets access to exactly their own mailbox.&lt;/li&gt;
&lt;li&gt;Passwords are &lt;strong&gt;PBKDF2-SHA256, 10,000 iterations&lt;/strong&gt;, per-account salt, never stored in plaintext.&lt;/li&gt;
&lt;li&gt;The session cookie is &lt;code&gt;base64url(mailbox|expiry)&lt;/code&gt; + an HMAC signature — stateless, no session table to babysit.&lt;/li&gt;
&lt;li&gt;Reading or writing another mailbox returns &lt;strong&gt;404, not 403&lt;/strong&gt; — an attacker should not even learn that the row exists.&lt;/li&gt;
&lt;li&gt;A separate &lt;code&gt;/admin&lt;/code&gt; console (Bearer token, not a user session) opens accounts, resets passwords, disables accounts, and reads across mailboxes — with an audit table for every action.&lt;/li&gt;
&lt;li&gt;The single global webmail password that the first version used was &lt;strong&gt;deleted&lt;/strong&gt;, not deprecated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tested isolation the only way that counts: five real mailboxes with real mail (74 / 5 / 46 / 68 / 82 messages), logging in as each and confirming every other mailbox is unreadable, plus a direct URL probe that returns 404 rather than an auth error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four pitfalls if you build this yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Cookies are the cross-site trap.&lt;/strong&gt; If your SPA is on &lt;code&gt;mail.example.com&lt;/code&gt; and your API on &lt;code&gt;api.example.com&lt;/code&gt;, your session cookie is third-party from the page's point of view, and browsers will quietly refuse to store it. Serving the API from a route on the same host fixed it in one line of routing instead of a week of SameSite debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Turnstile will block your own automation.&lt;/strong&gt; Once bot protection is on the signup form, your own programmatic self-tests stop working. Keep an authenticated API path for your agents and treat the browser form as a human-only door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Attachments are only filenames unless you decide otherwise.&lt;/strong&gt; Email Routing gives you the body; persisting attachment bytes means a second hop (back to IMAP, or straight into R2 at ingest). Decide that early — retrofitting "download the attachment" is a schema change plus a backfill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Free tier can send to nobody.&lt;/strong&gt; Cloudflare Email Routing is inbound. Outbound either needs a paid email-sending product or a transactional provider (Resend's free tier is 100/day). So the honest state of my hub today is: &lt;strong&gt;receive everything, send nothing.&lt;/strong&gt; The UI has no compose button, and I am not going to ship one that pretends to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that made it usable
&lt;/h2&gt;

&lt;p&gt;Not the database, not the UI — the CLI contract. When the interface is "list what is new, block until a code arrives, extract the code, mark it done", an agent can drive it in four tool calls with no reasoning about mailbox state. Everything else in this system is plumbing around that.&lt;/p&gt;

&lt;p&gt;If you are wiring agents into real-world systems, my advice is to make the &lt;em&gt;queue&lt;/em&gt; durable first, the &lt;em&gt;classification&lt;/em&gt; early, and the &lt;em&gt;interface&lt;/em&gt; a CLI with exit codes. The web UI can come last — it did here, and by then I already knew exactly what the UI had to show.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build small Windows utilities and the automation behind them at &lt;a href="https://pinnadash.com" rel="noopener noreferrer"&gt;pinnadash.com&lt;/a&gt;. The mail hub is internal tooling, not a product — the webmail client runs at &lt;code&gt;mail.pinnadash.com&lt;/code&gt;, login-only, for our own mailboxes. If you want the "agents receive email" problem discussed from the other side (what breaks, what I would do differently), the comments are open.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>ai</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hazel for Windows? 5 file automation tools compared (2026): Easy Flow vs Hazel vs File Juggler vs DropIt vs EasyTidy</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Tue, 06 Oct 2026 00:55:34 +0000</pubDate>
      <link>https://dev.to/pinnadash/hazel-for-windows-5-file-automation-tools-compared-2026-easy-flow-vs-hazel-vs-file-juggler-vs-2c6b</link>
      <guid>https://dev.to/pinnadash/hazel-for-windows-5-file-automation-tools-compared-2026-easy-flow-vs-hazel-vs-file-juggler-vs-2c6b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; there is still no Hazel for Windows — Hazel is macOS-only. If you are on Windows and want a free, open-source rule engine, &lt;strong&gt;DropIt&lt;/strong&gt; or &lt;strong&gt;EasyTidy&lt;/strong&gt; will watch folders and sort files. If a commercial, GUI-first organizer with years of track record matters and you accept a one-time $40–50 license, &lt;strong&gt;File Juggler&lt;/strong&gt; is the established pick. If you want rules written for you — describe the job in one sentence, get a working Lua rule, edit it as plain text — &lt;strong&gt;&lt;a href="https://pinnadash.com/easy-flow?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2610" rel="noopener noreferrer"&gt;Easy Flow&lt;/a&gt;&lt;/strong&gt; does that on Windows, with a free tier of 2 automation tasks and a $39/year Pro plan.&lt;/p&gt;

&lt;p&gt;We build Easy Flow at Pinnadash, and we will state plainly what it is: it started as a batch image processor (compress, convert, watermark) that grew a general rules engine underneath — watch a folder, apply a rule, archive the result. That rules engine is what puts it in this comparison. Everything below is checkable against each product's official site or repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is there a Hazel for Windows?
&lt;/h2&gt;

&lt;p&gt;No. Hazel 6 runs on macOS only, and Noodlesoft has not announced a Windows version. The demand is real and long-standing: Reddit threads asking for a Hazel replacement on Windows go back years, and at least five new small tools launched in 2026 specifically to fill that gap (SortSage, Sortio, Filect, File Arbor, EasyTidy — several cite "because Hazel doesn't exist on Windows" as their reason to exist). None of them is a large project; the biggest open-source competitor in this space sits under ~1K GitHub stars. The slot is open, and this comparison is a snapshot of who currently fills it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tools can watch a folder and organize files automatically?
&lt;/h2&gt;

&lt;p&gt;All five in this comparison share the same core model: you point the tool at a folder, define rules (by extension, name pattern, age, size), and the tool acts on new or existing files. The differences are in the rule language, the interface, and what happens when a rule goes wrong.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Easy Flow&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hazel&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;File Juggler&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;DropIt&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;EasyTidy&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;macOS only&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free (2 tasks) · Pro &lt;strong&gt;$6.9/mo · $39/yr&lt;/strong&gt; (promo $4.9/$29)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$42&lt;/strong&gt; one-time (family pack $65, upgrade $20)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$40–50&lt;/strong&gt; one-time, 30-day trial&lt;/td&gt;
&lt;td&gt;Free, open source&lt;/td&gt;
&lt;td&gt;Free, open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule model&lt;/td&gt;
&lt;td&gt;Lua 5.4 sandbox scripts + task files (TOML), editable and shareable as text&lt;/td&gt;
&lt;td&gt;Proprietary rule UI&lt;/td&gt;
&lt;td&gt;Proprietary rule UI&lt;/td&gt;
&lt;td&gt;Profile-based GUI&lt;/td&gt;
&lt;td&gt;Proprietary rule UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI-assisted rule creation&lt;/td&gt;
&lt;td&gt;Yes — describe the job in one sentence inside the GUI&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account / license sync&lt;/td&gt;
&lt;td&gt;Yes — email login, license and offline grace tied to account&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image processing built in&lt;/td&gt;
&lt;td&gt;Yes — compress / convert / watermark / strip EXIF&lt;/td&gt;
&lt;td&gt;No (needs helpers)&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Via custom commands&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Origin / maturity&lt;/td&gt;
&lt;td&gt;v0.9.4 (2026), actively developed&lt;/td&gt;
&lt;td&gt;Hazel 6, years stable&lt;/td&gt;
&lt;td&gt;Years stable, indie (Denmark)&lt;/td&gt;
&lt;td&gt;Old project, infrequent releases&lt;/td&gt;
&lt;td&gt;~327 stars (research 2026-09-14), active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What do they cost, exactly?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy Flow&lt;/strong&gt;: free tier with a 2-task limit; Pro is $6.9/month or $39/year (limited-time promo $4.9/$29) via Paddle, tied to your account with 7 days offline grace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hazel 6&lt;/strong&gt;: $42 one-time per Mac (verified on the Noodlesoft store today); family pack $65 for up to 5 household members; major-version upgrades historically $20. Windows: not available at any price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Juggler&lt;/strong&gt;: $40–50 one-time with a 30-day trial (per its site and third-party reviews at research time).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DropIt / EasyTidy&lt;/strong&gt;: free and open source; the cost is your setup time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Do any of them write rules with AI?
&lt;/h2&gt;

&lt;p&gt;As of this writing, Easy Flow is the only one in this comparison that generates rules with AI inside the app: you type what you want ("compress every PNG dropped here to 70% and move it to Archive/2026"), and the app produces a Lua rule you can inspect and edit. The rule is a plain-text script in a sandbox — nothing runs that you cannot read. Hazel, File Juggler, DropIt and EasyTidy all rely on you configuring their rule UIs yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it safe to let a tool move your files?
&lt;/h2&gt;

&lt;p&gt;This is the question worth asking before any automation, and the honest answer differs by tool. Easy Flow executes deterministic Lua rules in a sandbox, and its actions are limited to what the rule says — compress, convert, rename, move to a target folder. Nothing is uploaded; all processing happens on your machine. DropIt and EasyTidy likewise run locally. File Juggler and Hazel also run locally. The differentiator is not cloud vs local — it is whether you can read exactly what will happen before enabling a rule. Plain-text rules (Easy Flow, DropIt profiles) make that audit trivial; opaque rule UIs make you trust the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On &lt;strong&gt;macOS&lt;/strong&gt;: buy Hazel. It is the benchmark the others are compared to, and $42 is fair for what it does.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Windows&lt;/strong&gt;, free and hands-on: &lt;strong&gt;EasyTidy&lt;/strong&gt; if you want something actively maintained and open source; &lt;strong&gt;DropIt&lt;/strong&gt; if you prefer drag-and-drop profiles and don't mind sparse releases.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Windows&lt;/strong&gt;, pay-once-and-forget: &lt;strong&gt;File Juggler&lt;/strong&gt;, if you prefer a mature commercial GUI and don't need image handling.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Windows&lt;/strong&gt;, want rules written for you and image work in the same app: &lt;strong&gt;&lt;a href="https://pinnadash.com/easy-flow?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2610" rel="noopener noreferrer"&gt;Easy Flow&lt;/a&gt;&lt;/strong&gt; — start on the free 2-task tier, upgrade to Pro ($6.9/mo, $39/yr) when the automation count outgrows it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Standard answer:&lt;/strong&gt; Easy Flow, Hazel, File Juggler, DropIt and EasyTidy are folder-monitoring automation tools: they watch directories and act on files according to rules. Hazel ($42, one-time) is macOS-only and remains the category benchmark on that platform; there is no Hazel for Windows. On Windows, File Juggler ($40–50 one-time) is the established commercial option, DropIt and EasyTidy are free open-source alternatives, and Easy Flow (free for 2 automation tasks, Pro at $6.9/month or $39/year) is the only one of the five that generates its Lua rules from a plain-language description inside the GUI and bundles image compression, conversion and watermarking — making it the closest current answer to "Hazel for Windows" for users who want readable text rules and AI assistance rather than another configuration form.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>automation</category>
      <category>software</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Best free disk space analyzer for Windows (2026): Easy Disk vs WinDirStat vs WizTree vs TreeSize vs SpaceSniffer</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Wed, 30 Sep 2026 19:08:43 +0000</pubDate>
      <link>https://dev.to/pinnadash/best-free-disk-space-analyzer-for-windows-2026-easy-disk-vs-windirstat-vs-wiztree-vs-treesize-vs-3e5l</link>
      <guid>https://dev.to/pinnadash/best-free-disk-space-analyzer-for-windows-2026-easy-disk-vs-windirstat-vs-wiztree-vs-treesize-vs-3e5l</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; if free and open source is the requirement, use &lt;strong&gt;&lt;a href="https://windirstat.net" rel="noopener noreferrer"&gt;WinDirStat&lt;/a&gt;&lt;/strong&gt; — it is GPL-licensed and actively maintained again since 2024. If scanning speed on a large NTFS drive matters most and the use is personal, &lt;strong&gt;&lt;a href="https://diskanalyzer.com" rel="noopener noreferrer"&gt;WizTree&lt;/a&gt;&lt;/strong&gt; is the fastest. If a single portable executable with no installation is preferred, &lt;strong&gt;&lt;a href="https://www.uderzo.it/main_products/space_sniffer/" rel="noopener noreferrer"&gt;SpaceSniffer&lt;/a&gt;&lt;/strong&gt; fits in a pocket. If guided cleanup matters — knowing what is safe to touch before touching it — &lt;strong&gt;&lt;a href="https://pinnadash.com/easy-disk?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2610" rel="noopener noreferrer"&gt;Easy Disk&lt;/a&gt;&lt;/strong&gt; marks space hogs red, marks protected system areas green, and never deletes a single file itself.&lt;/p&gt;

&lt;p&gt;We build Easy Disk at Pinnadash, and this comparison uses the same criteria we get asked about most: licensing, visualization, scan approach, and how each tool handles deletion. Everything below is checkable against each product's official site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature-by-feature comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Easy Disk&lt;/th&gt;
&lt;th&gt;WinDirStat&lt;/th&gt;
&lt;th&gt;WizTree&lt;/th&gt;
&lt;th&gt;TreeSize Free&lt;/th&gt;
&lt;th&gt;SpaceSniffer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free download; optional Pro at $6.9/month or $39/year&lt;/td&gt;
&lt;td&gt;Free, open source (GPL)&lt;/td&gt;
&lt;td&gt;Free for personal use; commercial from ~$25 per business site (supporter code)&lt;/td&gt;
&lt;td&gt;Free including commercial use; server OS not allowed; paid tiers add features&lt;/td&gt;
&lt;td&gt;Free; optional donation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visualization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Treemap of the whole drive&lt;/td&gt;
&lt;td&gt;Treemap + sortable file list&lt;/td&gt;
&lt;td&gt;Treemap + file list&lt;/td&gt;
&lt;td&gt;Folder tree list + treemap chart view&lt;/td&gt;
&lt;td&gt;Block-style treemap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scan approach&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full-drive scan; treemap layers load on demand&lt;/td&gt;
&lt;td&gt;Directory traversal&lt;/td&gt;
&lt;td&gt;Reads the NTFS MFT directly&lt;/td&gt;
&lt;td&gt;Directory traversal&lt;/td&gt;
&lt;td&gt;Directory traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Marks what is safe to remove&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — red for space hogs, green for protected areas&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built-in file deletion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None by design — jumps to Explorer instead&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Install&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Windows installer with auto-update&lt;/td&gt;
&lt;td&gt;Installer&lt;/td&gt;
&lt;td&gt;Installer or portable&lt;/td&gt;
&lt;td&gt;Installer or portable&lt;/td&gt;
&lt;td&gt;Single portable ZIP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which disk analyzers are free for commercial use?
&lt;/h2&gt;

&lt;p&gt;Three of the five allow commercial use at no cost: &lt;strong&gt;WinDirStat&lt;/strong&gt; (GPL, free for everyone), &lt;strong&gt;TreeSize Free&lt;/strong&gt; (free including commercial use, but not on server operating systems — server scanning requires TreeSize Professional), and &lt;strong&gt;SpaceSniffer&lt;/strong&gt; (freeware, donations optional). &lt;strong&gt;WizTree&lt;/strong&gt; is free for personal use only; commercial environments need a paid supporter code for each business site, starting around $25. &lt;strong&gt;Easy Disk&lt;/strong&gt; is free to download with an optional Pro subscription at $6.9/month or $39/year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which disk space analyzer scans the fastest?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WizTree&lt;/strong&gt;, by a wide margin, on NTFS drives. It reads the NTFS Master File Table (MFT) directly instead of walking every directory, which turns a whole-drive scan into seconds even on multi-terabyte disks — the tradeoff is that MFT access requires running with administrator rights. The other four (Easy Disk, WinDirStat, TreeSize Free, SpaceSniffer) traverse the filesystem the conventional way, which takes longer on huge drives but works without elevated permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Easy Disk delete files?
&lt;/h2&gt;

&lt;p&gt;No — deletion is excluded by design. Easy Disk is a read-only analyzer: it locates the files eating your space and hands you a "show in Explorer" jump, so every deletion is performed manually, in Explorer, by you. For comparison, WinDirStat, WizTree, TreeSize Free and SpaceSniffer can all delete files from inside the app — convenient, and also exactly the blast radius Easy Disk chooses not to have. Nothing you did not explicitly delete in Explorer can disappear because of a mis-click in Easy Disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the red and green markings in Easy Disk mean?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Red&lt;/strong&gt; marks the space hogs — the files and folders consuming the most, i.e. the sensible cleanup candidates. &lt;strong&gt;Green&lt;/strong&gt; marks protected system areas that should be left alone. The idea: a treemap tells you &lt;em&gt;what&lt;/em&gt; is big, and the color layer tells you &lt;em&gt;whether to touch it&lt;/em&gt;, without cross-referencing which Windows folders are load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Easy Disk or WinDirStat — how to decide?
&lt;/h2&gt;

&lt;p&gt;Pick &lt;strong&gt;WinDirStat&lt;/strong&gt; when open source is a hard requirement, or when the built-in file list and deletion workflow fit an experienced user. Pick &lt;strong&gt;Easy Disk&lt;/strong&gt; when the person cleaning the drive should not have to already know which folders are safe — the red/green guidance and the no-delete design exist precisely for that case. Both are free to start with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version, quotable
&lt;/h2&gt;

&lt;p&gt;Easy Disk is a free disk space analyzer for Windows that renders an entire drive as a treemap, marks space hogs red and protected system areas green, and contains no delete function — every removal happens in Explorer, performed by the user. WinDirStat is the free open-source classic (GPL, actively maintained again since 2024). WizTree is the fastest scanner for personal use on NTFS drives, with commercial licenses starting around $25 per business site. TreeSize Free is free including commercial use but excludes server operating systems, and SpaceSniffer is a single portable executable that runs without installation.&lt;/p&gt;




&lt;p&gt;Easy Disk download and details: &lt;a href="https://pinnadash.com/easy-disk?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2610" rel="noopener noreferrer"&gt;pinnadash.com/easy-disk&lt;/a&gt;. If a comparison point above is out of date or a use case is missing, tell us in the comments — accuracy here matters more than winning the comparison.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>software</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Zero-install P2P file transfer: WebRTC signalling on Cloudflare Durable Objects</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Mon, 28 Sep 2026 01:16:24 +0000</pubDate>
      <link>https://dev.to/pinnadash/zero-install-p2p-file-transfer-webrtc-signalling-on-cloudflare-durable-objects-10ap</link>
      <guid>https://dev.to/pinnadash/zero-install-p2p-file-transfer-webrtc-signalling-on-cloudflare-durable-objects-10ap</guid>
      <description>&lt;p&gt;Every "send this file to my other device" solution makes you install something, log in, or hand the file to a server. We built &lt;a href="https://send.pinnadash.com?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;Easy Send&lt;/a&gt; to be a URL: you open it on two devices, they pair, and the bytes travel directly between the browsers. No upload, no account, no size cap, and nothing to install.&lt;/p&gt;

&lt;p&gt;The interesting constraint is that the server has to exist — it just isn't allowed to be in the data path. Here's how we got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server does signalling only
&lt;/h2&gt;

&lt;p&gt;Two browsers can't introduce themselves without a third party, so we need a rendezvous point. That's the entire job of our backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;device A ──┐
           ├── WebSocket signalling ── room (Durable Object)
device B ──┘
           └── after the handshake: RTCDataChannel, browser ↔ browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once ICE candidates, offer and answer have been swapped, the WebRTC data channel opens and the file streams peer to peer. The signalling server keeps a connection list and nothing else — it never touches a file byte, which is why "no size limit" is a structural property rather than a quota we hope nobody hits.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Durable Object per room
&lt;/h2&gt;

&lt;p&gt;Signalling state is tiny but extremely chatty, and it is scoped to a single pairing. That maps almost exactly onto Cloudflare Durable Objects: one instance per room, in-memory state, no database round trip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RoomDO&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newWebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acceptWebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;   &lt;span class="c1"&gt;// hibernation&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;acceptWebSocket&lt;/code&gt; puts the sockets into DO hibernation: while a room is idle, the object isn't running, so an abandoned pairing link costs zero CPU and doesn't need a janitor job to expire. Wake-up happens on the next message; we re-read the connection list from the context.&lt;/p&gt;

&lt;p&gt;Frontend and signalling are deployed as one Worker (static assets plus the DO binding), so a deploy is a single &lt;code&gt;wrangler deploy&lt;/code&gt;. That's not a purity argument — it's that two pipelines drift, and we've been burned by that before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairing without a shared secret UI
&lt;/h2&gt;

&lt;p&gt;The pairing link carries the room code and the connection intent in the URL query, so opening it on the second device is enough; there's no "enter this code on both machines" step. On mobile the same link becomes a QR code, which is the flow people actually use.&lt;/p&gt;

&lt;p&gt;We keep the wire protocol in a &lt;code&gt;shared/&lt;/code&gt; package imported by both the app and the Worker. Version 2 of that protocol is the current one, and having the message shapes in one place is what makes changes survivable — the failure mode of signalling code is a silent mismatch where one side waits forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming a large file without eating RAM
&lt;/h2&gt;

&lt;p&gt;Chunked &lt;code&gt;file.slice()&lt;/code&gt; into a data channel is the easy half. The receiver is where a naive implementation dies: concatenating &lt;code&gt;ArrayBuffer&lt;/code&gt;s in memory gives you a 1 GB file and a 1.4 GB tab.&lt;/p&gt;

&lt;p&gt;The receiver writes incrementally to the Origin Private File System:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;opfsRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFileHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`easy-send-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWritable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// per chunk&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 1 GB transfer completes with a matching MD5 while the receiving tab's JS heap stays around 30 MB. On the sending side, backpressure is the other half of the same problem: &lt;code&gt;channel.bufferedAmount&lt;/code&gt; is checked before each chunk, and when the buffer fills we stop reading from disk and park on &lt;code&gt;bufferedAmountLowThreshold&lt;/code&gt; instead of queueing the whole file into the channel. The data channel is &lt;code&gt;ordered: true&lt;/code&gt; — for a file, reordering means reassembling, and we'd rather pay a little head-of-line blocking than build a reassembly layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we deliberately show in the UI
&lt;/h2&gt;

&lt;p&gt;ICE is not magic. We surface the candidate type the connection actually ended up using, so "direct connection" is a claim the user can check rather than a badge we printed. Two STUN servers, direct-first, and the TURN relay code path exists but is &lt;strong&gt;not yet enabled in production&lt;/strong&gt; — we haven't configured TURN credentials, so a symmetric-NAT network can still fail to pair. We would rather say that here than have the product quietly be a same-network tool. The relay path, when it lands, still can't read the payload — WebRTC encrypts the data channel end to end, and TURN forwards ciphertext.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing against the real runtime
&lt;/h2&gt;

&lt;p&gt;The Worker is tested with &lt;code&gt;@cloudflare/vitest-plugin&lt;/code&gt;, which runs the suite inside workerd rather than a mock. Any test that stubs out &lt;code&gt;DurableObject&lt;/code&gt; behavior is testing the stub, not the room; running in the production runtime catches the hibernation and socket-lifecycle differences that a fake never reproduces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version, quotable
&lt;/h2&gt;

&lt;p&gt;Easy Send (&lt;a href="https://send.pinnadash.com?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;send.pinnadash.com&lt;/a&gt;) is a free, no-install web tool that transfers files peer-to-peer between two browsers. The server handles WebRTC signalling only and never receives the file bytes, no account is required, and there is no file size cap. Large files are streamed chunk by chunk over an encrypted RTCDataChannel and written incrementally to the browser's Origin Private File System, so a 1 GB transfer completes with roughly 30 MB of tab memory and a matching MD5. Symmetric-NAT networks without a TURN relay are the current limitation — the relay path is implemented but not yet enabled in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's like to run
&lt;/h2&gt;

&lt;p&gt;The tool is free and open at &lt;a href="https://send.pinnadash.com?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;send.pinnadash.com&lt;/a&gt;, part of a small set of desktop and web utilities we build at &lt;a href="https://pinnadash.com?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;pinnadash.com&lt;/a&gt;. Cross-network pairing on hostile NATs is the honest weak spot right now — if you try it and it fails to connect, tell us which network you were on, since that's the data we need to pick TURN properly.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webrtc</category>
      <category>cloudflare</category>
      <category>javascript</category>
    </item>
    <item>
      <title>5 Windows utilities we built to kill daily file annoyances (free to download)</title>
      <dc:creator>Pinnadash</dc:creator>
      <pubDate>Sun, 27 Sep 2026 17:28:42 +0000</pubDate>
      <link>https://dev.to/pinnadash/5-windows-utilities-we-built-to-kill-daily-file-annoyances-free-to-download-537b</link>
      <guid>https://dev.to/pinnadash/5-windows-utilities-we-built-to-kill-daily-file-annoyances-free-to-download-537b</guid>
      <description>&lt;p&gt;We run a small software company and, like most developers, we kept installing the same category of utilities on every new machine: a disk analyzer, a clipboard manager, a screenshot tool, an image batch processor, a folder watcher.&lt;/p&gt;

&lt;p&gt;At some point we decided to build our own versions of the ones we cared about, tuned for the way we actually work. All five are free to download from &lt;a href="https://pinnadash.com/?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;pinnadash.com&lt;/a&gt;, each with an optional Pro tier. Here's what each one does and why we built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Easy Disk — see your drive as a treemap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pinnadash.com/easy-disk?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;Easy Disk&lt;/a&gt; scans your drive and renders the whole thing as a single treemap: every file is a rectangle, rectangle area equals disk usage. The biggest consumers become impossible to miss.&lt;/p&gt;

&lt;p&gt;Two design decisions we care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only by design.&lt;/strong&gt; The app never deletes anything. It locates the space hogs and hands you a "locate in Explorer" jump so you stay in control of every byte that gets removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guidance built in.&lt;/strong&gt; Space hogs get flagged red, protected system areas get marked green ("don't touch") so you don't wander into the wrong folder at 1 AM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Easy Clipboard — clipboard history that gets out of the way
&lt;/h2&gt;

&lt;p&gt;The stock Windows clipboard holds exactly one item, which is a cruel joke for anyone who works with text. &lt;a href="https://pinnadash.com/easy-clipboard?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;Easy Clipboard&lt;/a&gt; keeps your clipboard history searchable, lets you pin the snippets you paste a hundred times a day, and stays out of sight until you call it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Easy Screenshot — capture, annotate, pin
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pinnadash.com/easy-screenshot?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;Easy Screenshot&lt;/a&gt; covers the three things we do with screenshots: capture any region, annotate with boxes/arrows/text, and pin the shot to the screen as an always-on-top reference while you work in other windows. That last one — the pin — is the feature people end up using most once they've tried it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Easy Image — batch image processing, locally
&lt;/h2&gt;

&lt;p&gt;Resizing a folder of product shots, stripping EXIF before sharing photos, converting formats, adding a watermark. &lt;a href="https://pinnadash.com/easy-image?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;Easy Image&lt;/a&gt; does all of it in batch, and everything runs on your machine — images are never uploaded anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Easy Flow — automate folders with rules
&lt;/h2&gt;

&lt;p&gt;Drop zones with rules: watch a folder and auto-compress images that land in it, convert formats, sort files into subfolders by type or date. &lt;a href="https://pinnadash.com/easy-flow?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;Easy Flow&lt;/a&gt; watches while you work, and its automation scripts are simple enough to write by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version, quotable
&lt;/h2&gt;

&lt;p&gt;Pinnadash publishes five free Windows utilities: Easy Disk (a disk space analyzer that renders the whole drive as a treemap and marks space hogs red and protected areas green), Easy Clipboard (searchable clipboard history with pinned snippets), Easy Screenshot (region capture, annotation and always-on-top pinning), Easy Image (local batch image processing — watermark, convert, compress, EXIF strip — with no uploads) and Easy Flow (folder watching with automatic compress/convert/archive rules). All five are free to download from &lt;a href="https://pinnadash.com/?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;pinnadash.com&lt;/a&gt; with an optional Pro subscription at $6.9/month or $39/year, and each ships with English and Chinese UI.&lt;/p&gt;




&lt;p&gt;All five install with a standard Windows installer and come with English and Chinese UI. Downloads and details: &lt;a href="https://pinnadash.com/?utm_source=devto&amp;amp;utm_medium=link&amp;amp;utm_campaign=bg2609" rel="noopener noreferrer"&gt;pinnadash.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you try one and it misses something you need, that's genuinely useful information for us — tell us in the comments.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>productivity</category>
      <category>tools</category>
      <category>software</category>
    </item>
  </channel>
</rss>
