<?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: Ops Panel</title>
    <description>The latest articles on DEV Community by Ops Panel (@opspanel_dev).</description>
    <link>https://dev.to/opspanel_dev</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%2F4239772%2F475000cf-dd7e-4bd1-8a16-b5d6e93dabcd.png</url>
      <title>DEV Community: Ops Panel</title>
      <link>https://dev.to/opspanel_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opspanel_dev"/>
    <language>en</language>
    <item>
      <title>git worktree remove --force emptied my main node_modules (Windows junctions)</title>
      <dc:creator>Ops Panel</dc:creator>
      <pubDate>Wed, 11 Nov 2026 20:54:07 +0000</pubDate>
      <link>https://dev.to/opspanel_dev/git-worktree-remove-force-emptied-my-main-nodemodules-windows-junctions-4fm1</link>
      <guid>https://dev.to/opspanel_dev/git-worktree-remove-force-emptied-my-main-nodemodules-windows-junctions-4fm1</guid>
      <description>&lt;p&gt;I let an AI agent build a deploy script for my own ops panel. The script created a &lt;code&gt;git worktree&lt;/code&gt;, built there, then cleaned up with &lt;code&gt;git worktree remove --force&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After the cleanup, the &lt;strong&gt;main&lt;/strong&gt; checkout's &lt;code&gt;node_modules&lt;/code&gt; was empty. Not the worktree's. The main one.&lt;/p&gt;

&lt;p&gt;Here is what actually happened, why recovery took 13 seconds, and the thing that scared me more than the outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that caused it
&lt;/h2&gt;

&lt;p&gt;The idea was reasonable: give the agent a scratch copy of the repo so it never touches the working tree I'm using.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo/                 &amp;lt;- main checkout
repo-build/           &amp;lt;- git worktree, created per deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;node_modules&lt;/code&gt; is big and slow to install, so the script made the worktree's &lt;code&gt;node_modules&lt;/code&gt; a &lt;strong&gt;junction&lt;/strong&gt; pointing at the main one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mklink /J repo-build\node_modules repo\node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A junction is the NTFS equivalent of a directory symlink. "The contents of this folder actually live over there."&lt;/p&gt;

&lt;p&gt;Then, at the end of the deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove --force repo-build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The finding: Git for Windows walks into junctions
&lt;/h2&gt;

&lt;p&gt;I reproduced it deliberately, because an accident you can't reproduce is a rumour.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git for Windows treats a junction as an ordinary directory and deletes its contents recursively.&lt;/strong&gt; It follows the link and removes the files on the other side — in the main checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rmdir&lt;/code&gt; in cmd does not follow it.&lt;/strong&gt; It removes the link itself and leaves the target alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same apparent operation ("delete this folder"), opposite blast radius, depending on which tool does the deleting. &lt;code&gt;--force&lt;/code&gt; meant no prompt and no warning on the way through.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post: &lt;strong&gt;do not create a link from a place that gets deleted to a place that matters.&lt;/strong&gt; Junctions, directory symlinks, bind mounts — the rule is the same. The link is not the only thing a cleanup routine will remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery was boring, and that was the point
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm install --frozen-lockfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;13 seconds. Done.&lt;/p&gt;

&lt;p&gt;It was fast because the &lt;strong&gt;lockfile was committed&lt;/strong&gt;. &lt;code&gt;node_modules&lt;/code&gt; is not data; it is a &lt;em&gt;build artifact&lt;/em&gt; with an exact recipe sitting in the repo. &lt;code&gt;--frozen-lockfile&lt;/code&gt; reinstalls precisely what the recipe says, or fails loudly.&lt;/p&gt;

&lt;p&gt;The outage was never the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: what if the junction had pointed at &lt;code&gt;data/&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;My panel keeps &lt;strong&gt;all of its business data as JSON files on local disk&lt;/strong&gt;. No database server. Orders, daily records, customer messages, configuration — files.&lt;/p&gt;

&lt;p&gt;And those files lived &lt;strong&gt;inside the same repository&lt;/strong&gt; the agent was creating and destroying worktrees in.&lt;/p&gt;

&lt;p&gt;The accident drew a line I had never drawn explicitly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;If deleted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reproducible&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;node_modules&lt;/code&gt;, build output, caches&lt;/td&gt;
&lt;td&gt;One command, with a lockfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not reproducible&lt;/td&gt;
&lt;td&gt;business JSON, hand-written config&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No way back&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I had been protecting the first category (lockfile committed, good) and doing nothing at all for the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Backups go &lt;em&gt;outside&lt;/em&gt; the repository
&lt;/h3&gt;

&lt;p&gt;Every 30 minutes, the durable JSON is copied to a timestamped directory &lt;strong&gt;outside the project tree&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%LOCALAPPDATA%\&amp;lt;app&amp;gt;\backups\data-YYYYMMDD-HHMMSS\
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Outside" matters more than "every 30 minutes." The lesson of the incident is that &lt;strong&gt;a deletion inside the repo can reach anything inside the repo&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;worktree cleanup&lt;/li&gt;
&lt;li&gt;an agent's own tidy-up command&lt;/li&gt;
&lt;li&gt;any deploy step that does "remove the folder, then recreate it"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;backup/&lt;/code&gt; folder inside the project is not a backup. It dies with everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The copy is crash-safe and bounded
&lt;/h3&gt;

&lt;p&gt;Two details that are easy to skip and annoying to discover later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write to a temp directory, then rename.&lt;/strong&gt; If the process dies mid-copy you get no snapshot rather than half a snapshot. Rename is atomic; a directory full of partially written JSON is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep N and prune.&lt;/strong&gt; Every 30 minutes is 48 directories a day. Decide the retention (I keep the last 24 ≈ 12 hours) and delete the rest, or you will find out when the disk fills.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also: &lt;strong&gt;exclude the churn.&lt;/strong&gt; Heartbeats, &lt;code&gt;*.pid&lt;/code&gt;, lock files, cached market data I can refetch in seconds — none of it belongs in a snapshot. One of my caches was 70 MB and turned 24 snapshots into 1.7 GB before I noticed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Restore once, on purpose
&lt;/h3&gt;

&lt;p&gt;A backup you have never restored is a hypothesis. Stop the app, copy a snapshot back over &lt;code&gt;data/&lt;/code&gt;, start it, confirm the screens are right. Until you do that, you have a directory of files, not a backup.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. No junctions in worktrees
&lt;/h3&gt;

&lt;p&gt;Each worktree now runs its own &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; and owns its &lt;code&gt;node_modules&lt;/code&gt;. The sharing was meant to save install time; the lockfile already made install fast. Trading tens of seconds for "a cleanup can reach into main" was a bad deal.&lt;/p&gt;

&lt;p&gt;(Separately, Turbopack rejects a &lt;code&gt;node_modules&lt;/code&gt; symlink pointing outside the project anyway, so the clever version was never going to survive.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bites harder with agents
&lt;/h2&gt;

&lt;p&gt;When you hand work to an agent, directories get created and destroyed while you are not watching. The commands are usually fine in isolation. What changes is that &lt;strong&gt;nobody is reading the &lt;code&gt;--force&lt;/code&gt; before it runs&lt;/strong&gt;, and the blast radius of a wrong assumption is no longer bounded by your attention.&lt;/p&gt;

&lt;p&gt;So the defence is structural, not behavioural:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproducible things are protected by a &lt;strong&gt;committed lockfile&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Non-reproducible things are copied &lt;strong&gt;outside the agent's reach&lt;/strong&gt;, automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No links from deletable places to important places.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this needs a tool. It is "put it in a different folder and copy it on a timer." I just wasn't doing it until something ate a directory in front of me.&lt;/p&gt;

&lt;p&gt;If you are running agents against a repo that also holds your real data: go look at where that data lives, and ask how you would get it back. That question took me 13 seconds to answer for &lt;code&gt;node_modules&lt;/code&gt; and uncomfortably long to answer for everything else.&lt;/p&gt;

</description>
      <category>git</category>
      <category>windows</category>
      <category>devops</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
