<?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: Hardil Singh</title>
    <description>The latest articles on DEV Community by Hardil Singh (@hardil_singh_08a1f0abf23d).</description>
    <link>https://dev.to/hardil_singh_08a1f0abf23d</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%2F2872913%2F97a1f5fb-4436-4dcb-b35e-af4518dbd023.jpg</url>
      <title>DEV Community: Hardil Singh</title>
      <link>https://dev.to/hardil_singh_08a1f0abf23d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hardil_singh_08a1f0abf23d"/>
    <language>en</language>
    <item>
      <title>My WSL2 VM Kept Losing Network Every Five Minutes</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 18:01:29 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/my-wsl2-vm-kept-losing-network-every-five-minutes-1319</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/my-wsl2-vm-kept-losing-network-every-five-minutes-1319</guid>
      <description>&lt;p&gt;Every five minutes or so, my entire Windows machine would drop off the network for a few seconds — not just WSL, the whole host. Browser tabs would stall, calls would drop, and it only happened while WSL2 was running. This is the story of finding that, plus the WSL memory-tuning landmines I hit right alongside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flapping
&lt;/h2&gt;

&lt;p&gt;The symptom was a host-wide network blip on a short, regular interval, correlated tightly with WSL2 being up. The cause: WSL2's default networking mode creates a virtual NAT switch on the Windows side, and on this machine that virtual switch was intermittently conflicting with the real network adapter — enough to cause the whole host to briefly renegotiate its connection.&lt;/p&gt;

&lt;p&gt;The fix was switching WSL2's networking mode entirely, via &lt;code&gt;.wslconfig&lt;/code&gt; (on Windows, not inside the Linux filesystem):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[wsl2]&lt;/span&gt;
&lt;span class="py"&gt;networkingMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mirrored&lt;/span&gt;
&lt;span class="py"&gt;dnsTunneling&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autoProxy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mirrored networking makes the WSL2 interface share the host's actual network identity instead of sitting behind a separate virtual NAT switch. You can confirm it actually took effect (rather than just trusting the config file) by checking, from inside WSL after a full restart, that its network interface holds the &lt;em&gt;same&lt;/em&gt; IP as the Windows host, that the default route points at the real LAN gateway rather than a private NAT range, and that loopback carries mirrored mode's marker address rather than a &lt;code&gt;172.x&lt;/code&gt; NAT address. If any of those don't match, the setting isn't actually active yet.&lt;/p&gt;

&lt;p&gt;Worth noting: this requires a reasonably recent WSL version and Windows build. If you're on an older one, mirrored mode may not be available at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The memory landmines, found the hard way
&lt;/h2&gt;

&lt;p&gt;Separately — and this had actually caused full VM crashes, not just hangs, at one point — I'd been carrying a few &lt;code&gt;.wslconfig&lt;/code&gt; settings that looked reasonable and were each, individually, a documented source of instability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An explicit &lt;code&gt;kernelCommandLine&lt;/code&gt; override.&lt;/li&gt;
&lt;li&gt;Resizing &lt;code&gt;swap&lt;/code&gt; past a few GB, which forces WSL to rebuild its virtual disk and can leave things in a bad state mid-rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[experimental] autoMemoryReclaim=gradual&lt;/code&gt; — has documented freeze bugs in the versions I was on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any one of these coinciding with a memory-constrained session was enough to take the WSL VM down hard enough that it wouldn't restart cleanly — the actual failure mode was a service-level "catastrophic failure" error, not just a slow hang.&lt;/p&gt;

&lt;p&gt;The safe way to adjust swappiness, instead of a kernel command-line flag, is a plain sysctl drop-in that gets applied on every boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/sysctl.d/99-swappiness.conf&lt;/span&gt;
vm.swappiness&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recovery, when it won't come back up at all
&lt;/h2&gt;

&lt;p&gt;If WSL refuses to launch after something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Close the terminal window outright. Don't keep hitting Enter — that just re-hits whatever service is already wedged.&lt;/li&gt;
&lt;li&gt;From PowerShell: &lt;code&gt;wsl --shutdown&lt;/code&gt;, then wait a beat before trying again.&lt;/li&gt;
&lt;li&gt;If it's still refusing: restart the underlying WSL service (as admin), then &lt;code&gt;wsl --update&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it's still unhappy: quit anything with its own WSL integration (a container runtime, for instance) and test again — integrations like that are a common second point of contention over the same resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A periodic, host-wide network blip that correlates with a VM being up is almost always that VM's networking mode, not your actual network hardware — don't start troubleshooting the router.&lt;/li&gt;
&lt;li&gt;Verify a networking-mode config change actually took effect (interface, route, loopback marker) instead of assuming the file being correct means the behavior changed.&lt;/li&gt;
&lt;li&gt;Some "obvious" tuning flags for a VM are documented landmines. Change one at a time, and check what's actually been reported broken about it before adding it.&lt;/li&gt;
&lt;li&gt;When recovery is needed, follow the same escalation path in order — don't jump straight to the most drastic fix before ruling out the cheap ones.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wsl</category>
      <category>linux</category>
      <category>networking</category>
      <category>windows</category>
    </item>
    <item>
      <title>What Durable Actually Means for Money-Critical Code</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 18:00:53 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/what-durable-actually-means-for-money-critical-code-5c7m</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/what-durable-actually-means-for-money-critical-code-5c7m</guid>
      <description>&lt;p&gt;Working on systems that move real money changes what "saved to the database" is allowed to mean. A row existing in Postgres is not the same claim as "this record is durable and this record is true." Here's the pattern I now apply to any code where a mutation has to be trusted after the fact — balances, ledgers, anything that gets audited.&lt;/p&gt;

&lt;h2&gt;
  
  
  One choke point, always
&lt;/h2&gt;

&lt;p&gt;Every mutation of critical state — a credit, a debit, a transfer, a reversal — goes through exactly one audited function. Not "usually," not "except for this one admin shortcut." One function, and every caller, including internal tooling, goes through it. The moment a second code path can also mutate the balance, the ledger stops being a source of truth and becomes one of several competing opinions about what happened.&lt;/p&gt;

&lt;p&gt;Caches are allowed to exist for read performance. They are never allowed to diverge from the ledger, and they're never the thing you trust when reconstructing history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Append-only and hash-chained, not just "logged"
&lt;/h2&gt;

&lt;p&gt;A row that can be edited or deleted after the fact isn't a record, it's a draft. Each ledger entry includes a hash of the previous entry, so any modification to history — not just deletion, but an in-place edit of an old row — breaks the chain and is immediately detectable. Where the integrity guarantee needs to be verifiable by someone outside the system (a counterparty, an auditor, a regulator), the chain gets signed, verifiable with a public key and no shared secret required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durable means it survives a crash between "accepted" and "written"
&lt;/h2&gt;

&lt;p&gt;A record isn't durable the moment your code decides to write it — it's durable once it's actually persisted somewhere that survives a crash. The gap between those two moments is where money quietly disappears in systems that don't think about it. The pattern: a durable enqueue with a local write-ahead-log fallback, so if the primary write path fails partway, nothing is silently dropped — it gets recovered and replayed on the next boot.&lt;/p&gt;

&lt;p&gt;Nothing that's genuinely "of record" is allowed to live only in memory. If a process restart can make a transaction vanish, it was never actually recorded — it was a promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sequence gaps are fine to detect; collisions are not fine to allow
&lt;/h2&gt;

&lt;p&gt;Sequence or ID design should make gaps &lt;em&gt;detectable&lt;/em&gt; — a monotonic counter where a skipped number is a visible anomaly worth investigating — while making collisions &lt;em&gt;structurally impossible&lt;/em&gt;, not just statistically unlikely. A detectable gap is an incident to review. A silent collision is corrupted history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoupling the hot path from the database
&lt;/h2&gt;

&lt;p&gt;Under load, the request/response path should never be gated on a synchronous database write. High-frequency or non-critical events go through a durable, backpressured queue with a single batched writer — bursts queue up, the database sees smooth load instead of a spike, and nothing on the user-facing path stalls waiting on persistence. The queue is still durable end to end; "decoupled" doesn't mean "best-effort."&lt;/p&gt;

&lt;h2&gt;
  
  
  Fairness needs the same rigor as money
&lt;/h2&gt;

&lt;p&gt;Anywhere an outcome has to be provably fair (a random draw, a game result, anything adjudicated after the fact), the seed has to come from a real CSPRNG, generated per-event — never a fixed value, and never derived from something observable or predictable like the system clock. Log the seed and the ruleset in force at the time, so a disputed outcome can actually be reconstructed and verified later, not just asserted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"It's in Postgres" is not the same claim as "it's durable." Ask what happens if the process dies one line after the write call.&lt;/li&gt;
&lt;li&gt;A single audited choke point isn't bureaucracy — it's the only way "reconstruct what happened from the ledger alone" stays true as the codebase grows.&lt;/li&gt;
&lt;li&gt;Build the tamper-evidence in from the start. Retrofitting a hash chain onto years of mutable history is a much harder project than doing it on day one.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>fintech</category>
      <category>database</category>
    </item>
    <item>
      <title>The Auth Pattern I Copy Into Every New Project Now</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 18:00:16 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/the-auth-pattern-i-copy-into-every-new-project-now-2p9d</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/the-auth-pattern-i-copy-into-every-new-project-now-2p9d</guid>
      <description>&lt;p&gt;I stopped deciding auth architecture from scratch on every new project. After enough rounds of the same debate — "can we just put the JWT in localStorage for now?" — I settled on one hardened pattern and now I just apply it every time, tweaking only where the app needs a Bearer-token escape hatch (mobile clients, third-party API consumers).&lt;/p&gt;

&lt;p&gt;Here's the pattern, and why each piece is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core decision: httpOnly cookie, never JS-readable storage
&lt;/h2&gt;

&lt;p&gt;The session token gets issued in an &lt;strong&gt;httpOnly + Secure + SameSite=Lax cookie&lt;/strong&gt;, domain-scoped so it works across subdomains. It never touches &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, or any JS-readable variable.&lt;/p&gt;

&lt;p&gt;The reason is simple: anything readable by JavaScript is readable by an XSS payload. An httpOnly cookie is invisible to &lt;code&gt;document.cookie&lt;/code&gt; and to any script running on the page, malicious or not. It's not a nice-to-have — it's the difference between "one XSS bug leaks a session" and "one XSS bug leaks nothing session-related."&lt;/p&gt;

&lt;h2&gt;
  
  
  CSRF double-submit, only where it's needed
&lt;/h2&gt;

&lt;p&gt;Cookie-based auth reintroduces CSRF risk that a Bearer token in a header doesn't have (a browser attaches cookies automatically; it doesn't automatically attach a custom header). So: a second, JS-&lt;strong&gt;readable&lt;/strong&gt; cookie carries a CSRF value, and the frontend echoes it back as a request header on every mutation. The server checks the two match. This only needs to guard &lt;em&gt;mutations&lt;/em&gt; — reads don't need it — and it doesn't apply at all to pure Bearer-token clients, since they were never vulnerable to it in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS: allowlist, never wildcard, especially with credentials
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; combined with &lt;code&gt;credentials: true&lt;/code&gt; is a combination that should never exist — it effectively lets any site ride your users' sessions. The fix is an explicit origin allowlist with &lt;code&gt;credentials: true&lt;/code&gt;, never a wildcard and never a reflected-origin shortcut that accepts whatever &lt;code&gt;Origin&lt;/code&gt; header shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  TOTP 2FA for anything privileged
&lt;/h2&gt;

&lt;p&gt;Any role with elevated access gets standard TOTP setup/enable/disable, required at login once enabled. Cheap to implement, disproportionately valuable for admin/privileged accounts specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating an existing Bearer-token app without breaking it mid-cutover
&lt;/h2&gt;

&lt;p&gt;This is the part that actually made the pattern practical to &lt;em&gt;adopt&lt;/em&gt;, not just design: when a live app already uses Bearer tokens, the migration goes &lt;strong&gt;dual-mode&lt;/strong&gt;. Auth guards accept either the httpOnly cookie or the &lt;code&gt;Authorization&lt;/code&gt; header; CSRF checks apply only to the cookie path. The login endpoint keeps returning the token in the response body (for existing Bearer clients) &lt;em&gt;while also&lt;/em&gt; setting the cookie. Nothing breaks for existing integrations while the frontend migrates over incrementally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification checklist, every time
&lt;/h2&gt;

&lt;p&gt;Before calling this "done" on a given app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login actually sets an httpOnly cookie (check response headers, not just "it seems to work").&lt;/li&gt;
&lt;li&gt;Cookie-based requests succeed without a manually attached token.&lt;/li&gt;
&lt;li&gt;A mutation request missing the CSRF header gets rejected (403), not silently allowed.&lt;/li&gt;
&lt;li&gt;A request from a disallowed origin gets no &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header at all.&lt;/li&gt;
&lt;li&gt;Logout actually clears both cookies, not just the session server-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skipping any one of those checks is how "we implemented CSRF protection" quietly becomes "we implemented a CSRF header nobody validates."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide the auth pattern once, well, and reuse it — don't re-litigate localStorage-vs-cookie on every new project.&lt;/li&gt;
&lt;li&gt;The CORS+credentials wildcard combination is the single easiest way to accidentally build a session-hijacking vulnerability. Make it impossible to ship, not just documented as wrong.&lt;/li&gt;
&lt;li&gt;A migration plan that keeps old clients working is what actually gets a hardened pattern adopted on a live app — "just rewrite it" proposals die in planning meetings.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>My Terminal Became an Ops Center, One Command at a Time</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:59:40 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/my-terminal-became-an-ops-center-one-command-at-a-time-6c</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/my-terminal-became-an-ops-center-one-command-at-a-time-6c</guid>
      <description>&lt;p&gt;None of this was designed. Every command in my shell profile exists because I got annoyed at typing the same multi-step sequence one too many times, and each one is small enough that it took twenty minutes to write and has saved hours since.&lt;/p&gt;

&lt;p&gt;Here's what accumulated, roughly in the order the annoyance struck:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;infra&lt;/code&gt; — server ops without memorizing which container is which
&lt;/h2&gt;

&lt;p&gt;I run a decent number of containers on a shared server across a few side projects. &lt;code&gt;docker ps&lt;/code&gt;, scrolling to find the right name, remembering the exact &lt;code&gt;docker exec&lt;/code&gt; incantation — fine once, tedious the fiftieth time. Now it's one word plus a fuzzy-picker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infra status     &lt;span class="c"&gt;# uptime, disk, mem, every container's health, one screen&lt;/span&gt;
infra ps &lt;span class="o"&gt;[&lt;/span&gt;name]  &lt;span class="c"&gt;# fuzzy-pick a container if no name given&lt;/span&gt;
infra logs / restart / &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;name]
infra stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;dev&lt;/code&gt; — stop typing "which package manager does this repo use"
&lt;/h2&gt;

&lt;p&gt;Different repos on my machine use different lockfiles, which means different install/run commands. &lt;code&gt;dev&lt;/code&gt; just detects the lockfile, &lt;code&gt;cd&lt;/code&gt;s into the project, and starts the right dev server — pnpm, yarn, npm, whichever it actually is. One word instead of "wait, is this the pnpm one or the npm one."&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;brief&lt;/code&gt; — the one-word morning check
&lt;/h2&gt;

&lt;p&gt;Server health, a genuine-unread email count (more on that below), dotfile backup status, and open pull requests — one command, run once, first thing. Before this existed my morning was four separate commands across three terminals just to answer "is anything on fire."&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;work&lt;/code&gt; — a consistent workspace per project
&lt;/h2&gt;

&lt;p&gt;Attaching to (or creating) a per-project terminal multiplexer session: one pane for a shell, one pre-loaded with the dev server, one running a terminal git UI. Same layout every time, for every project, instead of rebuilding the same three panes by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;funnel&lt;/code&gt; — exposing a local port to the internet, safely, on demand
&lt;/h2&gt;

&lt;p&gt;For testing webhooks (payment provider callbacks, that kind of thing) against code running on my own machine, I needed a real public HTTPS URL pointed at a local port, without standing up a deploy. A mesh VPN's "funnel" feature does exactly this — &lt;code&gt;funnel &amp;lt;port&amp;gt;&lt;/code&gt; streams requests live for a demo, &lt;code&gt;funnel bg &amp;lt;port&amp;gt;&lt;/code&gt; runs detached for something like a webhook listener, &lt;code&gt;funnel off&lt;/code&gt; tears it down. No DNS, no server, gone the moment I don't need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;inbox&lt;/code&gt; — a real signal-to-noise filter for email
&lt;/h2&gt;

&lt;p&gt;Fifteen years of accumulated newsletter/social noise makes a normal inbox useless as a signal. &lt;code&gt;inbox&lt;/code&gt; runs a targeted search (unread, primary category, recent, ignoring spam/promotions) and gives me an actual count of things that matter, plus a fuzzy-picker to read one without opening a mail client at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;cheat&lt;/code&gt; — because I kept forgetting my own tools
&lt;/h2&gt;

&lt;p&gt;The one that ties it together: a small, colorized cheat sheet of every command above, shown automatically on every new terminal. Building a dozen conveniences is pointless if you forget half of them exist three weeks later. This one's less about functionality and more about actually retaining the value of everything else on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The bar for "worth automating" isn't "this is hard" — it's "I've now typed this exact multi-step sequence three times."&lt;/li&gt;
&lt;li&gt;A one-word command with a fuzzy-picker beats memorizing exact names or flags every time.&lt;/li&gt;
&lt;li&gt;Build the reminder (&lt;code&gt;cheat&lt;/code&gt;) alongside the tools, not after. Tooling you forget about might as well not exist.&lt;/li&gt;
&lt;li&gt;Small, single-purpose shell functions compound. None of these took long individually; together they're most of my daily workflow now.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
      <category>linux</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Building a Claude Code Setup That Stops Me From Shooting Myself in the Foot</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:59:03 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/building-a-claude-code-setup-that-stops-me-from-shooting-myself-in-the-foot-3f50</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/building-a-claude-code-setup-that-stops-me-from-shooting-myself-in-the-foot-3f50</guid>
      <description>&lt;p&gt;Every time I hit a bug painful enough to remember, I've started asking the same question: how do I make sure my AI coding assistant never reintroduces it, even months from now, even in a repo it's never seen? Writing it down in a notes file isn't enough — notes get skimmed, context gets summarized, details get lost. So I built a small set of hooks that block specific, known-bad patterns outright, before they ever land.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: hard-won lessons should be enforced, not remembered
&lt;/h2&gt;

&lt;p&gt;A memory note is a suggestion. A hook that runs on every file write and blocks the write with a nonzero exit code is a rule. I only promote something to a hard rule once it's actually cost me real time — this isn't a wishlist of best practices, it's a short list of specific incidents.&lt;/p&gt;

&lt;p&gt;Right now that list blocks things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A specific dependency-import pattern that once pulled ~9,000 modules into a dev server's compile graph and hung an entire VM (the exact "convenient" import path stays blocked; importing the same package normally is fine).&lt;/li&gt;
&lt;li&gt;Container state getting bind-mounted into a repo's working tree as a root-owned directory — it silently breaks file watchers and formatters, and looks like an unrelated tooling bug when it happens.&lt;/li&gt;
&lt;li&gt;Writing private key material into any file.&lt;/li&gt;
&lt;li&gt;Wildcard or reflected CORS origins paired with &lt;code&gt;credentials: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Auth tokens written to &lt;code&gt;localStorage&lt;/code&gt;/&lt;code&gt;sessionStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An RNG seed derived from the system clock, anywhere near code that needs to be fair or unpredictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those started as "I can't believe that just happened" and ended as four lines in a hook script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guarding against a different failure mode: false confidence
&lt;/h2&gt;

&lt;p&gt;The blocking hook stops bad patterns from landing. A separate hook nudges against a subtler failure: declaring something "done" when it hasn't actually been proven. If there are uncommitted source changes in a git repo, it reminds me — once per session, not naggingly — to actually typecheck, run tests, and prove the change works end to end before calling it finished. A model (or a tired human) will happily say "this works" based on the code looking right. Proof is a different bar than plausibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specialists instead of one generalist re-deriving rules every time
&lt;/h2&gt;

&lt;p&gt;The other piece: instead of one assistant trying to hold every domain's rules in its head simultaneously, I split some review responsibilities into narrow specialist reviewers, invoked only when relevant — one that only checks money-handling invariants (does every mutation go through a single audited path, is anything computed in floats that shouldn't be), one that only reviews smart contracts, one that only checks a specific web framework's performance footguns, one that only checks auth/session patterns against a hardened standard. Each one is narrow on purpose. A generalist asked to check "everything" tends to check nothing very deeply; a specialist asked to check one thing tends to actually catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If something costs you real debugging time once, don't just write it down — figure out if it can be turned into something that mechanically blocks itself from happening again.&lt;/li&gt;
&lt;li&gt;Keep the blocklist short and specific. A rule for "don't do the exact thing that hurt me" is enforceable; a rule for "write good code" isn't.&lt;/li&gt;
&lt;li&gt;Separate "prevent bad patterns" from "verify claimed completion" — they're different failure modes and deserve different guardrails.&lt;/li&gt;
&lt;li&gt;Narrow, specialized reviewers beat one reviewer trying to hold every domain's rules at once.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devtools</category>
      <category>claude</category>
    </item>
    <item>
      <title>Why My Deploy Key Lives in a Password Manager, Not on Disk</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:57:15 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/why-my-deploy-key-lives-in-a-password-manager-not-on-disk-27l7</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/why-my-deploy-key-lives-in-a-password-manager-not-on-disk-27l7</guid>
      <description>&lt;p&gt;I have an SSH key that has never once existed as a file on my disk. It lives only inside an encrypted password manager vault, and every time it's used, I get a prompt to approve it. Here's why I set that up, and the near-miss that convinced me it was worth the extra friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The near-miss
&lt;/h2&gt;

&lt;p&gt;I once pasted a private key into an AI chat window and asked, essentially, "is this the right one?" It felt harmless — I just wanted to confirm an identity before deploying something. It wasn't harmless: the moment a private key is pasted anywhere — a chat, a file, a terminal that logs history — it has to be treated as compromised, because that transcript persists and gets processed elsewhere.&lt;/p&gt;

&lt;p&gt;Fortunately it was a freshly generated, not-yet-deployed key. I rotated it immediately — regenerated it, deployed the new one, deleted the old one everywhere — before it was ever used for anything real. But it was a clean demonstration of the actual rule: &lt;strong&gt;verify a key's identity by its fingerprint, never by looking at (or pasting) the raw key material.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-lf&lt;/span&gt; ~/.ssh/some_key.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a fingerprint you can compare against what you expect, with zero exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger fix: keys that don't touch disk at all
&lt;/h2&gt;

&lt;p&gt;That incident pushed me to stop keeping my most sensitive deploy key as a plain file at all. My password manager's desktop app already runs its own SSH agent — it can hold a key, unlocked only when I approve it, without ever writing the private material to a file the OS or any process can just read.&lt;/p&gt;

&lt;p&gt;The only wrinkle: my dev environment is WSL2, and the password manager's agent lives on the Windows side. Bridging the two took two small pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npiperelay.exe&lt;/code&gt;&lt;/strong&gt; on the Windows side, which exposes a Windows named pipe (the password manager's agent pipe) in a way a Linux process can talk to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;socat&lt;/code&gt;&lt;/strong&gt; on the WSL side, relaying that into a Unix domain socket.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# WSL side, roughly&lt;/span&gt;
socat UNIX-LISTEN:&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.ssh/agent-bridge.sock,fork &lt;span class="se"&gt;\&lt;/span&gt;
  EXEC:&lt;span class="s2"&gt;"npiperelay.exe //./pipe/openssh-ssh-agent"&lt;/span&gt;,nofork &amp;amp;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SSH_AUTH_SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.ssh/agent-bridge.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-started (guarded so it doesn't spawn duplicates) from my shell profile, so it's just always there. Verify it's working with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that specific deploy key exists in exactly one place — the vault — and gets served to &lt;code&gt;ssh&lt;/code&gt; only on unlock, with an approval prompt every time it's used. If my laptop disk were imaged tomorrow, that key wouldn't be on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers, deliberately not collapsed into one
&lt;/h2&gt;

&lt;p&gt;I keep a clear line between two things that are easy to accidentally merge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime secrets&lt;/strong&gt; — the ones tools actually read day to day (API tokens, most SSH keys) — live in gitignored, &lt;code&gt;chmod 600&lt;/code&gt; env files, auto-loaded by my shell profile. Fast, no unlock friction, appropriate for things that aren't catastrophic if a single machine is compromised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The password manager&lt;/strong&gt; — backup/escrow, human daily-driver, and the &lt;em&gt;one&lt;/em&gt; channel for anything where the blast radius of exposure is severe enough to justify unlock friction on every use (like that one deploy key).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deliberately didn't architect the runtime layer to fetch everything from the vault on every command. It sounds more secure, but it isn't, really — it just adds friction, and anything a tool or an assistant &lt;em&gt;uses&lt;/em&gt; ends up in its working context regardless of which vault it came from. The vault's real value is as an audited, approval-gated path for the handful of secrets where that friction is actually worth paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never paste a private key anywhere, even to "just check" something — verify by fingerprint instead.&lt;/li&gt;
&lt;li&gt;If a key is ever exposed, rotating it immediately isn't overreacting — it's the only response that actually closes the exposure.&lt;/li&gt;
&lt;li&gt;Not every secret needs vault-level friction. Reserve it for the few keys where "approve every single use" is a feature, not an annoyance.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>ssh</category>
      <category>productivity</category>
      <category>wsl</category>
    </item>
    <item>
      <title>The SSH Lockout That Made Me Close Port 22 For Good</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:57:13 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/the-ssh-lockout-that-made-me-close-port-22-for-good-3cde</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/the-ssh-lockout-that-made-me-close-port-22-for-good-3cde</guid>
      <description>&lt;p&gt;I got locked out of my own production server. Not "forgot the password" locked out — both SSH and the hosting panel's browser console rejected me, on a box running several live apps, for hours.&lt;/p&gt;

&lt;p&gt;Here's what actually happened, and the two lessons that came out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong assumption that cost the first two hours
&lt;/h2&gt;

&lt;p&gt;My hosting provider's browser-based rescue console kept rejecting &lt;code&gt;root&lt;/code&gt;'s password, even right after resetting it through their panel. I assumed the reset was broken and spent ages fighting it.&lt;/p&gt;

&lt;p&gt;It wasn't broken. On a standard Ubuntu cloud image, &lt;strong&gt;the account the rescue console logs in as is &lt;code&gt;ubuntu&lt;/code&gt;, not &lt;code&gt;root&lt;/code&gt;&lt;/strong&gt; — a convention from how cloud images provision the default user. Root's password can be perfectly correct and still be useless there. On top of that, my &lt;code&gt;ubuntu&lt;/code&gt; account turned out to be &lt;strong&gt;locked&lt;/strong&gt; (&lt;code&gt;passwd -S&lt;/code&gt; showed &lt;code&gt;L&lt;/code&gt;) since the box was created, completely unrelated to any password value.&lt;/p&gt;

&lt;p&gt;Two separate, unglamorous facts, neither documented anywhere I'd looked, together cost me two hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second problem, stacked on top
&lt;/h2&gt;

&lt;p&gt;Separately — and this part I never fully root-caused — direct SSH and HTTP to the server's public IP was timing out entirely, not rejecting auth, just hanging. I ruled out the firewall (0 rules at the panel level), fail2ban (ban window had long expired). My best guess: something about the source IP I was connecting from, which rotates on a home/mobile connection. It resolved itself once that IP changed.&lt;/p&gt;

&lt;p&gt;The apps themselves never went down — I checked via an external monitor the whole time. Only my own access was blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual recovery
&lt;/h2&gt;

&lt;p&gt;When both normal SSH and the console login are dead, most hosting panels have an "Emergency Mode" or rescue mode: it boots a minimal OS and mounts your real disk read/write somewhere like &lt;code&gt;/mnt&lt;/code&gt;. From there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk                       &lt;span class="c"&gt;# find the real disk, not the rescue OS's own&lt;/span&gt;
mount &lt;span class="nt"&gt;--bind&lt;/span&gt; /dev  /mnt/sdX/dev
mount &lt;span class="nt"&gt;--bind&lt;/span&gt; /proc /mnt/sdX/proc
mount &lt;span class="nt"&gt;--bind&lt;/span&gt; /sys  /mnt/sdX/sys
&lt;span class="nb"&gt;chroot&lt;/span&gt; /mnt/sdX /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the chroot, check lock status before touching anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;passwd &lt;span class="nt"&gt;-S&lt;/span&gt; ubuntu     &lt;span class="c"&gt;# L = locked, P = usable&lt;/span&gt;
passwd &lt;span class="nt"&gt;-u&lt;/span&gt; ubuntu      &lt;span class="c"&gt;# unlock&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'ubuntu:newpassword'&lt;/span&gt; | chpasswd   &lt;span class="c"&gt;# NOT interactive `passwd` here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last point matters more than it looks: pasting a password into an &lt;em&gt;interactive&lt;/em&gt; &lt;code&gt;passwd&lt;/code&gt; prompt from a rescue-mode terminal garbled it badly for me the first time. &lt;code&gt;chpasswd&lt;/code&gt; piped from &lt;code&gt;echo&lt;/code&gt; is the reliable way to do it non-interactively.&lt;/p&gt;

&lt;p&gt;One more trap: a lot of panels offer a one-click "reset firewall configuration." It's not a targeted unblock — it fully disables the firewall. Don't reach for it as a first move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that actually matters
&lt;/h2&gt;

&lt;p&gt;The real fix wasn't unlocking accounts — it was removing the single point of failure that caused this: SSH access tied to a rotating public IP with no fallback path.&lt;/p&gt;

&lt;p&gt;I installed &lt;strong&gt;Tailscale&lt;/strong&gt; on the server and on every device I administer it from. Now admin access goes over the tailnet's stable private IP, completely independent of whatever public IP my ISP hands me that day. I deliberately didn't enable Tailscale's own SSH server — sshd stays the single SSH auth surface, Tailscale just becomes the network path to reach it.&lt;/p&gt;

&lt;p&gt;Once every CI pipeline that deployed to this box was migrated to connect the same way — join the tailnet as a short-lived tagged node, then SSH over the stable internal IP — I closed the public port 22 rule entirely. SSH is Tailscale-only now, full stop.&lt;/p&gt;

&lt;p&gt;One gotcha worth flagging if you do this: a &lt;strong&gt;tagged&lt;/strong&gt; device in Tailscale's ACL is not automatically covered by the default "logged-in members can reach each other" rule. A CI node can join the tailnet fine and still get connection timeouts on the actual SSH attempt, because the tag was never explicitly granted access in the ACL policy. It needs its own explicit grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On a cloud VPS, check the &lt;em&gt;actual&lt;/em&gt; console account before assuming it's root — it usually isn't.&lt;/li&gt;
&lt;li&gt;Don't let a single rotating IP be your only path to a server. Put a stable, IP-independent access layer (a mesh VPN like Tailscale, or equivalent) in place &lt;em&gt;before&lt;/em&gt; you need it, not while you're locked out.&lt;/li&gt;
&lt;li&gt;Tagged/automated identities need explicit ACL grants — "it joined the network" and "it can reach anything" are two different guarantees.&lt;/li&gt;
&lt;li&gt;A locked-out admin panel doesn't mean a down service — verify what's actually broken (access vs. the app itself) before you start firefighting.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>ssh</category>
      <category>vps</category>
    </item>
    <item>
      <title>One Icon Import Nearly Took Down My Entire WSL2 Dev Environment</title>
      <dc:creator>Hardil Singh</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:55:21 +0000</pubDate>
      <link>https://dev.to/hardil_singh_08a1f0abf23d/one-icon-import-nearly-took-down-my-entire-wsl2-dev-environment-2l01</link>
      <guid>https://dev.to/hardil_singh_08a1f0abf23d/one-icon-import-nearly-took-down-my-entire-wsl2-dev-environment-2l01</guid>
      <description>&lt;p&gt;One afternoon my mouse froze. Not the app — the whole WSL window. &lt;code&gt;Ctrl+C&lt;/code&gt; did nothing. &lt;code&gt;docker ps&lt;/code&gt; in another pane just spun. Windows Task Manager showed &lt;code&gt;vmmemWSL&lt;/code&gt; sitting at the ceiling and not moving. The only way out was closing every WSL terminal and running &lt;code&gt;wsl --shutdown&lt;/code&gt; from PowerShell, then waiting for the VM to fully die before starting it back up.&lt;/p&gt;

&lt;p&gt;The trigger, once I found it, was almost funny: I'd added an icon pack for a crypto UI and imported it the "easy" way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Next.js 16 on Turbopack, running inside WSL2. The host machine has 16GB of RAM, and I deliberately cap the WSL VM in &lt;code&gt;.wslconfig&lt;/code&gt; — a hard memory and swap ceiling — instead of trusting WSL2's default "grow until the host chokes" behavior. Windows needs headroom too; if the VM doesn't have a ceiling, it'll take all of it.&lt;/p&gt;

&lt;p&gt;That ceiling is usually a non-issue. &lt;code&gt;next dev&lt;/code&gt; compiles routes on demand, memory rises a bit as you click around, and it settles. Until it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I needed a handful of chain/token icons and reached for a popular icon package's &lt;code&gt;/dynamic&lt;/code&gt; entry point — the one that lets you resolve an icon by symbol at runtime instead of importing each one by name. Convenient. Also, it turns out, a way to hand your dev compiler close to &lt;strong&gt;9,000 modules&lt;/strong&gt; it didn't need.&lt;/p&gt;

&lt;p&gt;Turbopack (like Webpack before it) doesn't know you only want three icons — a dynamic/barrel entry point exposes the &lt;em&gt;whole&lt;/em&gt; package as one graph, and the dev compiler has to walk all of it to build the module map, even before you ever click a page that uses it. RSS climbed, GC couldn't keep up, and once we hit the &lt;code&gt;.wslconfig&lt;/code&gt; ceiling the VM stopped being responsive to anything, including the shell I was trying to diagnose it from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The red herring
&lt;/h2&gt;

&lt;p&gt;Here's the part that cost me the most time: &lt;code&gt;next build&lt;/code&gt; was completely fine. Green, fast, no warnings.&lt;/p&gt;

&lt;p&gt;That's because a production build compiles the graph &lt;strong&gt;once and exits&lt;/strong&gt; — it pays the cost and moves on. &lt;code&gt;next dev&lt;/code&gt; instead keeps that graph resident and re-touches it on every file change, for the entire life of the process. A passing build tells you nothing about whether &lt;code&gt;next dev&lt;/code&gt; is going to survive an afternoon of work. I'd been checking the wrong signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the actual cause
&lt;/h2&gt;

&lt;p&gt;Once I stopped trusting the build and started watching the dev server itself — &lt;code&gt;ps&lt;/code&gt; on the &lt;code&gt;next-server&lt;/code&gt; process, RSS over time, and which route/import triggered the jump — the pattern was obvious: it wasn't a leak, it was a &lt;strong&gt;baseline&lt;/strong&gt;. The dev server just needed a genuinely huge chunk of memory to hold that module graph, permanently, because that's what the import pulled in.&lt;/p&gt;

&lt;p&gt;And it wasn't unique to that one icon package. Any "import one thing from a big indexed package" pattern does this:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Icon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some-huge-icon-pack/dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// pulls the whole pack&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@radix-ui/react-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;     &lt;span class="c1"&gt;// barrel export&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useReactTable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// same story&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next ships a default list of packages it already knows to optimize (&lt;code&gt;lucide-react&lt;/code&gt; is one), but component-level libraries like individual &lt;code&gt;@radix-ui/*&lt;/code&gt; packages and &lt;code&gt;@tanstack/*&lt;/code&gt; are &lt;strong&gt;not&lt;/strong&gt; on that list by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Stop pulling the whole graph for a few icons.&lt;/strong&gt; Dropped the &lt;code&gt;/dynamic&lt;/code&gt; entry point entirely and either imported the specific icons by name or used dependency-free inline glyphs where a handful of icons didn't justify a whole package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tell Turbopack explicitly what to optimize.&lt;/strong&gt; In &lt;code&gt;next.config.ts&lt;/code&gt;:&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="nx"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;optimizePackageImports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lucide-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@radix-ui/react-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// every other barrel/indexed import actually in use&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;This rewrites those imports to their direct file paths at compile time, so the dev compiler only touches what's actually used instead of the whole package. On this repo it took &lt;code&gt;next-server&lt;/code&gt;'s steady-state RSS from &lt;strong&gt;~3.6GB down to ~1.87GB&lt;/strong&gt; — about 48% less, just from that one config change. I verified it wasn't a fluke by hammering every route 10x plus sitting idle and watching RSS stay flat afterward — this is a baseline drop, not a leak fix.&lt;/p&gt;

&lt;p&gt;One thing that does &lt;strong&gt;not&lt;/strong&gt; help, because it's solving the wrong problem: &lt;code&gt;--max-old-space-size&lt;/code&gt;. Turbopack's memory is native Rust, not the V8 heap — bumping the Node heap flag has zero effect on it. The only lever that actually works is fewer/lighter deps plus &lt;code&gt;optimizePackageImports&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cap the blast radius, don't just avoid it.&lt;/strong&gt; Even with the imports fixed, I kept the explicit &lt;code&gt;.wslconfig&lt;/code&gt; memory/swap ceiling. The goal isn't "never hit a limit" — it's "when something does regress, it degrades instead of hanging the whole VM and taking the host down with it."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A green &lt;code&gt;next build&lt;/code&gt; proves nothing about &lt;code&gt;next dev&lt;/code&gt; health — they pay completely different costs. Verify dev separately: hammer routes, then let it idle, and watch RSS actually settle.&lt;/li&gt;
&lt;li&gt;Treat any &lt;code&gt;/dynamic&lt;/code&gt; entry point or barrel/index import from a sizable package as a suspect, not a convenience — check what it actually pulls in before wiring it up.&lt;/li&gt;
&lt;li&gt;If you're on WSL2, set an explicit memory + swap ceiling in &lt;code&gt;.wslconfig&lt;/code&gt; rather than letting the VM grow unbounded. It won't stop a bad import from being a bad import, but it stops one bad import from being a full-system hang.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;next dev&lt;/code&gt; freezes and nothing responds, don't fight it — &lt;code&gt;wsl --shutdown&lt;/code&gt; from Windows and start clean. Diagnose after, with the process actually killed.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>wsl</category>
      <category>turbopack</category>
    </item>
  </channel>
</rss>
