<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nodejs on Lessons learned</title><link>https://eghuro.cz/tags/nodejs/</link><description>Recent content in Nodejs on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Mon, 06 Jul 2026 15:04:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/nodejs/index.xml" rel="self" type="application/rss+xml"/><item><title>ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY: your coding agent's pnpm 11 is not your pnpm 10</title><link>https://eghuro.cz/posts/pnpm-11-agent-runtime-no-tty/</link><pubDate>Mon, 06 Jul 2026 15:04:00 +0200</pubDate><guid>https://eghuro.cz/posts/pnpm-11-agent-runtime-no-tty/</guid><description>&lt;p&gt;A coding agent ran &lt;code&gt;pnpm test&lt;/code&gt; in my repo and got this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Scope: all 6 workspace projects&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY] Aborted removal of modules directory due to no TTY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;If you are running pnpm in CI, set the CI environment variable to &amp;#34;true&amp;#34;, or set &amp;#34;confirmModulesPurge&amp;#34; to &amp;#34;false&amp;#34;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The stack trace ended in &lt;code&gt;runDepsStatusCheck (.../node_modules/.pnpm/pnpm@11.7.0/...)&lt;/code&gt;. My &lt;code&gt;package.json&lt;/code&gt; says &lt;code&gt;&amp;quot;packageManager&amp;quot;: &amp;quot;pnpm@10.33.0&amp;quot;&lt;/code&gt;, and &lt;code&gt;pnpm -v&lt;/code&gt; in my terminal prints &lt;code&gt;10.33.0&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;If you see this error only inside an agent, the agent is running its own pnpm, and following the hint makes things worse.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS, pnpm 10.33.0 through Corepack, a pnpm monorepo with a Next.js 15 app&lt;/li&gt;&#10;&lt;li&gt;The Codex desktop app, which ships its own Node 24 and pnpm under &lt;code&gt;~/.cache/codex-runtimes/codex-primary-runtime/dependencies/&lt;/code&gt;: pnpm 11.7.0 in July, 11.25.0 in October&lt;/li&gt;&#10;&lt;li&gt;The agent runs &lt;code&gt;pnpm test&lt;/code&gt; without a TTY&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;The agent followed the hint and reran with &lt;code&gt;CI=true pnpm test&lt;/code&gt;. pnpm stopped asking and did what it had wanted to do from the start:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Recreating /Users/me/project/node_modules&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Progress: resolved 378, reused 0, downloaded 137, added 12&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[WARN] GET https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz error (ERR_PNPM_ENOSPC). Will retry in 10 seconds. 2 retries left.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[WARN] GET https://registry.npmjs.org/next/-/next-15.5.19.tgz error (ERR_PNPM_ENOSPC). Will retry in 10 seconds. 2 retries left.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Progress: resolved 378, reused 0, downloaded 377, added 377, done&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: cpu-features@0.0.10, esbuild@0.21.5, esbuild@0.28.1, protobufjs@7.6.4, sharp@0.34.5, ssh2@1.17.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It deleted &lt;code&gt;node_modules&lt;/code&gt; and downloaded every package again (&lt;code&gt;reused 0&lt;/code&gt;) into a second store, and the disk ran out on the way. It left an untracked &lt;code&gt;.pnpm-store/&lt;/code&gt; in the repo and wrote placeholders into the tracked &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;allowBuilds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cpu-features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;set this to true or false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;set this to true or false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;protobufjs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;set this to true or false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;set this to true or false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ssh2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;set this to true or false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No tests ran. After that, even &lt;code&gt;pnpm exec tsc --noEmit&lt;/code&gt; failed with the same NO_TTY error, so the agent switched to &lt;code&gt;./node_modules/.bin/tsc&lt;/code&gt; and &lt;code&gt;./node_modules/.bin/vitest&lt;/code&gt;, reverted the YAML, deleted &lt;code&gt;.pnpm-store/&lt;/code&gt; and committed with only the focused tests green.&lt;/p&gt;&#10;&lt;p&gt;Ten weeks later, &lt;code&gt;next build&lt;/code&gt; failed:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ⚠ Attempted to load @next/swc-darwin-arm64, but it was not installed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ⨯ Failed to load SWC binary for darwin/arm64, see more info here: https://nextjs.org/docs/messages/failed-loading-swc&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The July install had ended one package short (378 resolved, 377 added), and the SWC binary was one of the two downloads that hit &lt;code&gt;ENOSPC&lt;/code&gt;. A later agent session &amp;ldquo;fixed&amp;rdquo; the build by installing the binary into &lt;code&gt;/tmp&lt;/code&gt; and symlinking it into &lt;code&gt;node_modules&lt;/code&gt;, a fix that lasts until macOS cleans &lt;code&gt;/tmp&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;First, ask the agent&amp;rsquo;s shell which pnpm it gets:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;command&lt;/span&gt; -v pnpm&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pnpm -v&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then make the agent use your version. Corepack reads &lt;code&gt;packageManager&lt;/code&gt;, whatever is first on &lt;code&gt;PATH&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;corepack pnpm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="c1"&gt;# runs pnpm 10.33.0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Write that into &lt;code&gt;AGENTS.md&lt;/code&gt; (or your agent&amp;rsquo;s equivalent), along with one rule: if pnpm wants to remove &lt;code&gt;node_modules&lt;/code&gt;, stop and report.&lt;/p&gt;&#10;&lt;p&gt;To make the bundled pnpm harmless as well, turn off the automatic install in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;verifyDepsBeforeRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is the pnpm 10 default, so nothing changes for you. I checked it with the bundled pnpm 11.25.0: with the setting, it runs the script; without it, it installs first.&lt;/p&gt;&#10;&lt;p&gt;If you already followed the hint, clean up with your own pnpm:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git checkout -- pnpm-workspace.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf .pnpm-store&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;corepack pnpm install --frozen-lockfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run that in a terminal. If pnpm asks to remove &lt;code&gt;node_modules&lt;/code&gt;, say yes; this time it rebuilds against your own store.&lt;/p&gt;&#10;&#10;&lt;h2 id="why"&gt;Why &lt;/h2&gt;&#10;&lt;p&gt;Three pnpm 11 changes line up:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;verify-deps-before-run&lt;/code&gt; defaults to &lt;code&gt;install&lt;/code&gt; (pnpm 10: &lt;code&gt;false&lt;/code&gt;). &lt;code&gt;pnpm test&lt;/code&gt; and &lt;code&gt;pnpm exec&lt;/code&gt; check &lt;code&gt;node_modules&lt;/code&gt; first and install if anything is off.&lt;/li&gt;&#10;&lt;li&gt;pnpm 11 uses store &lt;code&gt;v11&lt;/code&gt;; pnpm 10 uses &lt;code&gt;v10&lt;/code&gt;. &lt;code&gt;node_modules/.modules.yaml&lt;/code&gt; records the store it was linked from, so to pnpm 11 a working pnpm 10 install looks incompatible and has to be purged. &lt;code&gt;confirmModulesPurge&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;; a purge without a TTY aborts, and &lt;code&gt;CI=true&lt;/code&gt; drops the confirmation but not the purge.&lt;/li&gt;&#10;&lt;li&gt;Ignored build scripts are an error, and pnpm writes the &lt;code&gt;allowBuilds&lt;/code&gt; placeholders for you to fill in.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;On my Mac, the two stores now sit side by side in &lt;code&gt;~/Library/pnpm/store/&lt;/code&gt;: 851 MB in &lt;code&gt;v10&lt;/code&gt;, 588 MB in &lt;code&gt;v11&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Why didn&amp;rsquo;t &lt;code&gt;packageManager&lt;/code&gt; catch it? pnpm 11 does honor it: run the bundled binary directly and &lt;code&gt;pnpm -v&lt;/code&gt; prints &lt;code&gt;10.33.0&lt;/code&gt;, because it switches to the pinned version. The wrapper in the October runtime turns that off:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dirname &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;pnpm_config_pm_on_fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ignore&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/../../node/bin/node&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/../../node/node_modules/pnpm/bin/pnpm.mjs&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The environment variable beats anything in the repo. With &lt;code&gt;pmOnFail: error&lt;/code&gt; in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, or &lt;code&gt;devEngines.packageManager&lt;/code&gt; with &lt;code&gt;&amp;quot;onFail&amp;quot;: &amp;quot;error&amp;quot;&lt;/code&gt;, the wrapper still runs 11.25.0.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;An agent runtime can bring its own toolchain and switch off the check that would have caught it. When an agent reports &amp;ldquo;tests blocked by the environment&amp;rdquo;, find out which binary it ran before you believe the report.&lt;/p&gt;&#10;&lt;p&gt;Run &lt;code&gt;pnpm -v&lt;/code&gt; in your agent&amp;rsquo;s shell today and compare it with &lt;code&gt;packageManager&lt;/code&gt;.&lt;/p&gt;&#10;</description></item><item><title>TS2724 "has no exported member" in a git worktree: a symlinked node_modules typechecks the main checkout's source</title><link>https://eghuro.cz/posts/worktree-symlinked-node-modules-stale-workspace-package/</link><pubDate>Mon, 22 Jun 2026 19:21:00 +0200</pubDate><guid>https://eghuro.cz/posts/worktree-symlinked-node-modules-stale-workspace-package/</guid><description>&lt;p&gt;A coding agent cherry-picked a subagent&amp;rsquo;s commit into an integration worktree, ran &lt;code&gt;pnpm typecheck&lt;/code&gt; and got:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apps/web/lib/uploads.ts(3,59): error TS2724: &amp;#39;&amp;#34;@app/persistence&amp;#34;&amp;#39; has no exported member named &amp;#39;recordRawUpload&amp;#39;. Did you mean &amp;#39;recordUpload&amp;#39;?&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;recordRawUpload&lt;/code&gt; was right there, exported from &lt;code&gt;packages/persistence/src/index.ts&lt;/code&gt; in that worktree. If tsc can&amp;rsquo;t see an export that you can, and the worktree&amp;rsquo;s &lt;code&gt;node_modules&lt;/code&gt; is a symlink to another checkout, tsc is reading the other checkout&amp;rsquo;s source.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A pnpm 10 monorepo on macOS. The workspace packages &lt;code&gt;@app/*&lt;/code&gt; point &lt;code&gt;exports&lt;/code&gt; straight at &lt;code&gt;./src/index.ts&lt;/code&gt;, so there is no build step between packages.&lt;/li&gt;&#10;&lt;li&gt;An orchestrating agent that creates a git worktree, a branch, and a subagent per issue, and cherry-picks finished work into an integration worktree.&lt;/li&gt;&#10;&lt;li&gt;A full disk.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;Every worktree had its own install, and &lt;code&gt;du&lt;/code&gt; counted up to 513 MB of &lt;code&gt;node_modules&lt;/code&gt; in each. A dozen worktrees later, the disk was full:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem Size Used Avail Capacity iused ifree %iused Mounted on&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/disk3s5 460Gi 420Gi 116Mi 100% 6.6M 1.2M 85% /System/Volumes/Data&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To save space, the agent replaced the &lt;code&gt;node_modules&lt;/code&gt; of each new worktree (the root, &lt;code&gt;apps/web&lt;/code&gt; and every package) with a symlink to the main checkout&amp;rsquo;s copy:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ln -s ../project/node_modules ../project-integration/node_modules&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ln -s ../../../project/apps/web/node_modules ../project-integration/apps/web/node_modules&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first typecheck after that passed. Later, a change that added an export to &lt;code&gt;@app/persistence&lt;/code&gt; and used it in &lt;code&gt;apps/web&lt;/code&gt; failed with TS2724. &lt;code&gt;readlink&lt;/code&gt; shows why:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; readlink apps/web/node_modules/@app/persistence&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;../../../../packages/persistence&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;pnpm links workspace packages with relative symlinks. A relative link resolves from the directory it really sits in, and that directory is now in the main checkout. So &lt;code&gt;@app/persistence&lt;/code&gt; in the worktree was &lt;code&gt;/Users/me/project/packages/persistence&lt;/code&gt;, the main checkout&amp;rsquo;s source, which didn&amp;rsquo;t have the new function yet.&lt;/p&gt;&#10;&lt;p&gt;The typecheck that passed also validated the main checkout.&lt;/p&gt;&#10;&lt;p&gt;Two subagents in their own worktrees hit the same error. One deleted the links, reinstalled, and its typecheck passed. The other wrote &amp;ldquo;Validation blocked by worktree node_modules links targeting older shared package. Source diff reviewed; commit now.&amp;rdquo; and committed without a passing typecheck.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;Give each worktree its own install. pnpm keeps package files in a content-addressable store, so an offline install only links them into place:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm node_modules apps/web/node_modules packages/*/node_modules &lt;span class="c1"&gt;# the symlinks&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pnpm install --offline --frozen-lockfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This took 3.6 seconds and reported &lt;code&gt;resolved 378, reused 378, downloaded 0&lt;/code&gt;. If the store is missing a package, &lt;code&gt;--offline&lt;/code&gt; fails; drop the flag.&lt;/p&gt;&#10;&lt;p&gt;Then check that every workspace package resolves inside the worktree. Run it with bash: zsh aborts on the first glob that matches nothing (&lt;code&gt;no matches found&lt;/code&gt;), and the agent&amp;rsquo;s own fix hit exactly that.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Print workspace packages that resolve outside this checkout.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt; -P&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; link in node_modules/@app/* */*/node_modules/@app/*&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;realpath &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$target&lt;/span&gt; in &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;/*&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; *&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No output means every workspace package comes from this worktree.&lt;/p&gt;&#10;&lt;p&gt;If you have to share &lt;code&gt;node_modules&lt;/code&gt;, share only the third-party packages. The agent&amp;rsquo;s fix was an overlay: a real &lt;code&gt;node_modules&lt;/code&gt; directory per package, with every entry symlinked to the main checkout except &lt;code&gt;@app&lt;/code&gt;, whose entries point at the worktree&amp;rsquo;s own packages. Copying the main checkout&amp;rsquo;s &lt;code&gt;@app&lt;/code&gt; directory with &lt;code&gt;cp -RP&lt;/code&gt; gives the same result, because the copied relative links resolve from their new place.&lt;/p&gt;&#10;&#10;&lt;h2 id="why"&gt;Why &lt;/h2&gt;&#10;&lt;p&gt;TypeScript (with the default &lt;code&gt;preserveSymlinks: false&lt;/code&gt;) and Node both resolve a package to its real path. Third-party packages are the same files in every checkout that shares a lockfile, so following the link to the main checkout is harmless. Workspace packages are your source code, and that differs between checkouts by design. Once a branch changes its dependencies, the third-party half goes stale as well.&lt;/p&gt;&#10;&lt;p&gt;The disk savings were probably smaller than they looked. pnpm clones or hard-links files from its store, and &lt;code&gt;du&lt;/code&gt; counts those blocks again in every tree, so its per-worktree numbers overstate what each install cost. The disk was also full for other reasons, and nobody measured how much the links saved.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;A worktree that borrows another checkout&amp;rsquo;s &lt;code&gt;node_modules&lt;/code&gt; gets that checkout&amp;rsquo;s workspace packages too, and typecheck and tests then run against the wrong code, passing or failing. Run &lt;code&gt;pnpm install --offline&lt;/code&gt; in every new worktree.&lt;/p&gt;&#10;&lt;p&gt;If your agents create worktrees, run the check above in one of them today.&lt;/p&gt;&#10;</description></item></channel></rss>