<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nextjs on Lessons learned</title><link>https://eghuro.cz/tags/nextjs/</link><description>Recent content in Nextjs 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>Tue, 06 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://eghuro.cz/tags/nextjs/index.xml" rel="self" type="application/rss+xml"/><item><title>Next.js Server Action arguments are Flight data: use error digests carefully</title><link>https://eghuro.cz/posts/nextjs-server-action-args-are-flight-data-error-digest-oracle/</link><pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/nextjs-server-action-args-are-flight-data-error-digest-oracle/</guid><description>&lt;p&gt;A Next.js Server Action can receive a JSON-looking body whose strings are interpreted as React Flight tokens. If ordinary input works but &lt;code&gt;$&lt;/code&gt;-prefixed input changes the response or produces HTTP 500, inspect the decoded arguments before trying more SQL or shell payloads. Differences between error digests can help separate failure paths, but they cannot establish a vulnerable version or remote code execution on their own.&lt;/p&gt;&#10;&lt;p&gt;This matters when testing the request behind an action call. The normal client serializer escapes literal strings for the protocol; manually editing the wire body can change what the server decodes.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-application-sees-the-result-of-decodereply"&gt;The application sees the result of decodeReply &lt;/h2&gt;&#10;&lt;p&gt;In the tagged &lt;a&#10; href="https://github.com/vercel/next.js/blob/v15.0.0/packages/next/src/server/app-render/action-handler.ts"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Next.js 15.0.0 action handler&lt;/a&gt;&#10;, fetch-action bodies pass through &lt;code&gt;decodeReply&lt;/code&gt; or its streaming counterpart before the selected action runs. The &lt;a&#10; href="https://github.com/facebook/react/blob/v19.0.0/packages/react-server/src/ReactFlightReplyServer.js"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;React 19.0.0 reply decoder&lt;/a&gt;&#10; interprets &lt;code&gt;$&lt;/code&gt;-prefixed strings, including escaped literals, special numbers, and references.&lt;/p&gt;&#10;&lt;p&gt;For a local decoder, these argument values illustrate the distinction:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Wire string inside the arguments array&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Decoded argument&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ordinary&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;string &lt;code&gt;ordinary&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;$$literal&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;string &lt;code&gt;$literal&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;$Infinity&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;number &lt;code&gt;Infinity&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;$-Infinity&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;number &lt;code&gt;-Infinity&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;$undefined&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;A value changing type before the application function runs explains why an otherwise boring search action can behave strangely. Input validation inside that function does not protect an earlier, vulnerable deserializer.&lt;/p&gt;&#10;&#10;&lt;h2 id="reproduce-the-protocol-locally-before-probing-further"&gt;Reproduce the protocol locally before probing further &lt;/h2&gt;&#10;&lt;p&gt;Use an isolated scratch directory. The following intentionally installs an old, vulnerable package for an offline decoder experiment. Do not deploy it, expose a listener, or feed it untrusted traffic. Delete the directory afterward.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LAB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="p"&gt;:?Set TMPDIR to your scratch directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/flight-lab.XXXXXX&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="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LAB&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;npm install --ignore-scripts --no-audit --no-fund --save-exact &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; react@19.0.0 react-dom@19.0.0 react-server-dom-webpack@19.0.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save this as &lt;code&gt;check.cjs&lt;/code&gt; in that directory, then run &lt;code&gt;node --conditions=react-server check.cjs&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:assert/strict&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;decodeReply&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;react-server-dom-webpack/server.node&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#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;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ordinary&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ordinary&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$$literal&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;$literal&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$Infinity&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$-Infinity&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$undefined&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&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; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;decodeReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;wire&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&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="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;5 Flight decoding cases passed&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This checks protocol behavior, not exploitability. For an authorized application assessment, reproduce the actual resolved packages and runtime conditions. Next.js may bundle its React implementation; a standalone npm package is not automatically an exact framework replica.&lt;/p&gt;&#10;&lt;p&gt;Preserve the decoded root&amp;rsquo;s expected shape, too. The tagged Next.js handler invokes &lt;code&gt;actionHandler.apply(null, boundActionArguments)&lt;/code&gt;. A root that resolves to a primitive string fails that call&amp;rsquo;s array-like argument requirement before the intended action behavior can be tested. Keep transport decoding and action invocation as separate checkpoints.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-an-error-digest-can-tell-you"&gt;What an error digest can tell you &lt;/h2&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://github.com/vercel/next.js/blob/v15.0.0/packages/next/src/server/app-render/create-error-handler.tsx"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Next.js 15.0.0 error handler&lt;/a&gt;&#10; generates digests using &lt;code&gt;stringHash&lt;/code&gt; over error text and stack information in several paths, unless a digest already exists. One path uses:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stringHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Different parser branches can throw from different stack locations and yield different digests even when the visible error message is generic. Record the complete response, status, and digest for each controlled input; repeat baseline requests to check stability. Correlate differences with logs in the local replica.&lt;/p&gt;&#10;&lt;p&gt;A digest is a lossy fingerprint. Build paths, package versions, runtime stacks, and existing application digests can alter it. Distinct digests suggest distinct error material; matching digests do not prove identical execution paths. HTTP 500 alone does not even locate the failure in Flight decoding.&lt;/p&gt;&#10;&#10;&lt;h2 id="separate-protocol-reachability-from-react2shell"&gt;Separate protocol reachability from React2Shell &lt;/h2&gt;&#10;&lt;p&gt;React&amp;rsquo;s &lt;a&#10; href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;CVE-2025-55182 advisory&lt;/a&gt;&#10; confirms unauthenticated RCE through a flaw in decoding Server Function payloads. Ordinary Flight tokens also exist in correct protocol handling. Observing the transformations above therefore does not diagnose that CVE.&lt;/p&gt;&#10;&lt;p&gt;For exploit validation, first reproduce the affected decoder in a private replica and demonstrate a harmless, deterministic effect there. Keep that result separate from any authorized remote confirmation. The experiment above deliberately stops at benign decoding.&lt;/p&gt;&#10;&lt;p&gt;For prevention, follow the current React and framework advisories, update the resolved server-side packages, rebuild, and verify what the running artifact actually contains. Do not use the historical demonstration versions as a patch recommendation. Authenticate and authorize actions independently, validate decoded arguments, and avoid exposing decoded-input debug echoes. Those controls do not replace fixing a vulnerable decoder.&lt;/p&gt;&#10;&lt;p&gt;When a &lt;code&gt;$&lt;/code&gt; probe changes behavior, capture the full response and reproduce that transformation locally before assigning a CVE or an impact rating.&lt;/p&gt;&#10;</description></item><item><title>Sandbox-blocked tests called "environmental", then committed</title><link>https://eghuro.cz/posts/sandbox-blocked-tests-called-environmental/</link><pubDate>Mon, 13 Jul 2026 15:38:00 +0200</pubDate><guid>https://eghuro.cz/posts/sandbox-blocked-tests-called-environmental/</guid><description>&lt;p&gt;My coding agent finished a feature, ran the full test suite, and reported:&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;Full suite: 135 passed, 97 skipped; 22 DB suites failed due to no container runtime, plus sandboxed stub bind. Commit only handoff implementation files.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then it committed. I ran &lt;code&gt;pnpm test&lt;/code&gt; myself and got three real failures. The production build it had also skipped was broken too, and nobody found out for two months.&lt;/p&gt;&#10;&lt;p&gt;If your agent&amp;rsquo;s test run shows &lt;code&gt;Error: Could not find a working container runtime strategy&lt;/code&gt; or &lt;code&gt;Error: listen EPERM: operation not permitted 0.0.0.0:8080&lt;/code&gt;, those tests never ran, and a summary that counts them as &amp;ldquo;blocked&amp;rdquo; tells you nothing about the code.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Codex CLI on macOS, with its default sandbox&lt;/li&gt;&#10;&lt;li&gt;A pnpm monorepo: a Next.js 15.5.19 app plus a few packages&lt;/li&gt;&#10;&lt;li&gt;Vitest 2.1.9, with Testcontainers 10.28.0 starting Postgres for the database tests and a local stub of an external service listening on port 8080&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The agent split the feature into five slices and gave each to a parallel subagent. Each subagent ran its own focused tests, and the orchestrator ran the full suite and the build at the end.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;The sandbox blocks more than writes outside the project. Here it blocked three things the checks needed:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Testcontainers couldn&amp;rsquo;t reach Docker: &lt;code&gt;Could not find a working container runtime strategy&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;The service stub couldn&amp;rsquo;t bind its port: &lt;code&gt;listen EPERM: operation not permitted 0.0.0.0:8080&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;next build&lt;/code&gt; couldn&amp;rsquo;t write its compiler cache: &lt;code&gt;EPERM&lt;/code&gt; writing &lt;code&gt;~/Library/Caches/next-swc&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Every database suite failed in &lt;code&gt;beforeAll&lt;/code&gt;, and Vitest reported the tests inside them as skipped. The agent&amp;rsquo;s final report was accurate:&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;- Typecheck pass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Focused tests pass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Full: 135 passed; DB suites blocked by no Docker&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Build blocked by Next SWC cache permissions&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every line of that is true. But the suite has 232 tests, and 135 + 97 = 232: over 40 percent of it never ran, including every test that touched the database, where this feature did most of its work.&lt;/p&gt;&#10;&lt;p&gt;On the host, the same suite printed:&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;Test Files 2 failed | 63 passed (65)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Tests 3 failed | 229 passed (232)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One new test forgot to start the service stub, so its client found no endpoint configured. Two older tests broke because the feature added an ownership check and their fixtures sent requests without a logged-in user (&lt;code&gt;Error: authentication required&lt;/code&gt;). The agent fixed all three in two minutes once it saw them.&lt;/p&gt;&#10;&lt;p&gt;The build was worse. The commit re-exported a server action from another &lt;code&gt;&amp;quot;use server&amp;quot;&lt;/code&gt; file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;use server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#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;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;finalizeArtifact&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;../../reports/actions.ts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;tsc&lt;/code&gt; accepts this, and so do the tests. Only &lt;code&gt;next build&lt;/code&gt; rejects it:&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;Failed to compile.&#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;./app/reports/[id]/actions.ts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Error: x Only async functions are allowed to be exported in a &amp;#34;use server&amp;#34; file.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two months later, a different session ran a production build, hit this error, and deleted the line.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-same-sandbox-the-same-day"&gt;The same sandbox, the same day &lt;/h2&gt;&#10;&lt;p&gt;Half an hour before the bad commit, another session in the same repository hit the same two errors. At first it called them environmental too. Then it decided:&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;Full suite blocked by sandbox: Testcontainers cannot see Docker; stub bind gets `EPERM :8080`. Since Docker is reportedly running, retry full suite with host access.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two minutes later it reported &amp;ldquo;Host rerun green: 60 files, 222 tests&amp;rdquo;, and only then did it commit. The agent that fixed my three failures did the same: it reran the focused tests with host approval before committing the fix. Both sessions knew how to leave the sandbox; nothing required them to before a commit.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;Treat &amp;ldquo;blocked&amp;rdquo; as &amp;ldquo;not run&amp;rdquo;, and make &amp;ldquo;not run&amp;rdquo; stop the commit.&lt;/p&gt;&#10;&lt;p&gt;In Codex, a command runs outside the sandbox when the agent requests &lt;code&gt;sandbox_permissions: &amp;quot;require_escalated&amp;quot;&lt;/code&gt; and you approve it. Put the rule in the instructions file the agent reads every session (&lt;code&gt;AGENTS.md&lt;/code&gt;, or &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;A check that the sandbox blocked has not passed. Before committing, rerun every blocked test suite and build with host access (&lt;span class="sb"&gt;`sandbox_permissions: &amp;#34;require_escalated&amp;#34;`&lt;/span&gt;).&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;If that isn&amp;#39;t possible, don&amp;#39;t commit: report what didn&amp;#39;t run.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then read the agent&amp;rsquo;s numbers yourself. If passed plus skipped equals the total, and you never skip tests on purpose, every skipped test is one that didn&amp;rsquo;t run. If you have CI, have it run the full suite and the build before a merge.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-it-happens"&gt;Why it happens &lt;/h2&gt;&#10;&lt;p&gt;The agent&amp;rsquo;s memory for this project already had a rule: &amp;ldquo;separate passed static or focused checks from runtime suites blocked by the environment, and reserve the final green claim for integrated verification.&amp;rdquo; The agent followed that rule to the letter: it never claimed green and listed exactly what had been blocked. Then it committed, because nothing in its instructions tied the commit to the green claim.&lt;/p&gt;&#10;&lt;p&gt;&amp;ldquo;Blocked by the environment&amp;rdquo; reads like an infrastructure problem that someone else will handle, so it ends up as a footnote in the report while the commit goes ahead. The rule made the report honest. It didn&amp;rsquo;t make anyone run the tests.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;When a sandbox blocks a test or a build, nothing was verified. An agent that reports this honestly will still commit, unless its instructions say that a blocked check stops the commit.&lt;/p&gt;&#10;&lt;p&gt;Look at your agent&amp;rsquo;s last commit: if its summary says a suite or a build was &amp;ldquo;blocked&amp;rdquo;, rerun them on the host today.&lt;/p&gt;&#10;</description></item><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>Next.js "Cannot read properties of undefined (reading 'length')" in installChunk: two processes share .next</title><link>https://eghuro.cz/posts/nextjs-installchunk-shared-next-dir/</link><pubDate>Mon, 22 Jun 2026 13:01:00 +0200</pubDate><guid>https://eghuro.cz/posts/nextjs-installchunk-shared-next-dir/</guid><description>&lt;p&gt;I opened the dashboard of my Next.js app and got a 500 error:&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; GET /app/dashboard 500 in 285ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ⨯ TypeError: Cannot read properties of undefined (reading &amp;#39;length&amp;#39;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at &amp;lt;unknown&amp;gt; (.next/server/app/app/accounts/page.js:685:47)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at Object.&amp;lt;anonymous&amp;gt; (.next/server/app/app/accounts/page.js:688:3) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; page: &amp;#39;/app/accounts&amp;#39;&#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; GET /app/accounts 500 in 278ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the stack points into generated &lt;code&gt;.next/server/.../page.js&lt;/code&gt; and the full trace ends in &lt;code&gt;installChunk&lt;/code&gt; in &lt;code&gt;webpack-runtime.js&lt;/code&gt;, your code is fine. Another Next process has written into the &lt;code&gt;.next&lt;/code&gt; directory your dev server is reading from.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Next.js 15.5 with webpack in dev, in a pnpm monorepo (the app lives in &lt;code&gt;apps/web&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;pnpm dev&lt;/code&gt; running in my terminal on port 3000&lt;/li&gt;&#10;&lt;li&gt;A coding agent working in the same checkout, which checks its work with &lt;code&gt;pnpm build&lt;/code&gt;&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;I asked the agent to fix the 500. It did what anyone would do with &lt;code&gt;reading 'length'&lt;/code&gt;: it grepped the app for &lt;code&gt;.length&lt;/code&gt;. The accounts page didn&amp;rsquo;t touch a collection at all, so the agent followed the authenticated layout into the auth code and suspected an import there. Then it opened the generated &lt;code&gt;page.js&lt;/code&gt; at line 685 and found webpack glue, not application code.&lt;/p&gt;&#10;&lt;p&gt;To reproduce, the agent ran &lt;code&gt;pnpm build&lt;/code&gt;, which succeeded, and then started its own &lt;code&gt;next dev -p 3001&lt;/code&gt; next to mine. The first request to &lt;code&gt;/app/accounts&lt;/code&gt; returned 200. After a request to the dashboard, both routes returned 500, and this time the error page carried the whole stack:&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;TypeError: Cannot read properties of undefined (reading &amp;#39;length&amp;#39;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at installChunk (apps/web/.next/server/webpack-runtime.js:249:41)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; at __webpack_require__.f.require (apps/web/.next/server/webpack-runtime.js:259:15)&#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; at apps/web/.next/server/app/app/dashboard/page.js:718:47&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Line 249 is in webpack&amp;rsquo;s chunk loader:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;installChunk&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;moreModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...&#10;&lt;/span&gt;&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;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;chunkIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;installedChunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;chunkIds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;chunk&lt;/code&gt; is whatever &lt;code&gt;require(&amp;quot;./&amp;quot; + chunkFile)&lt;/code&gt; returned. &lt;code&gt;chunk.ids&lt;/code&gt; was undefined, so the file on disk was not the chunk this runtime expected. A &lt;code&gt;ps&lt;/code&gt; showed why: two &lt;code&gt;next-server&lt;/code&gt; processes, mine on 3000 and the agent&amp;rsquo;s on 3001, both compiling into &lt;code&gt;apps/web/.next&lt;/code&gt;. On top of that, the agent&amp;rsquo;s &lt;code&gt;next build&lt;/code&gt; had written its production output into the same directory a few minutes earlier.&lt;/p&gt;&#10;&lt;p&gt;Both of the usual ways to reproduce a bug, a fresh build and a second server, are ways to cause this one. Before you reproduce, look for other Next processes on the checkout:&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;ps -axo pid,ppid,command &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;&amp;#39;[n]ext-server&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;Give &lt;code&gt;next dev&lt;/code&gt; its own output directory in &lt;code&gt;next.config.mjs&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// `next dev` and `next build` generate incompatible webpack chunk graphs.&#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;// Keep their output separate so a build cannot break a running dev server.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;distDir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;development&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.next-dev&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.next&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ignore it in &lt;code&gt;.gitignore&lt;/code&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-gitignore" data-lang="gitignore"&gt;.next/&#10;.next-dev/&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add its generated types to &lt;code&gt;tsconfig.json&lt;/code&gt;, next to the &lt;code&gt;.next&lt;/code&gt; ones:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;include&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;next-env.d.ts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;**/*.ts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;**/*.tsx&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.next/types/**/*.ts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.next-dev/types/**/*.ts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The running dev server picked up the config change and rebuilt into &lt;code&gt;.next-dev&lt;/code&gt;. From then on, &lt;code&gt;next build&lt;/code&gt; writes to &lt;code&gt;.next&lt;/code&gt; and the dev server never reads it.&lt;/p&gt;&#10;&lt;p&gt;To recover without the config change, stop every Next process except one, delete &lt;code&gt;.next&lt;/code&gt; and restart &lt;code&gt;pnpm dev&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re on Next.js 16, you already have this: &lt;code&gt;next dev&lt;/code&gt; writes to &lt;code&gt;.next/dev&lt;/code&gt;, so &lt;a&#10; href="https://nextjs.org/docs/app/api-reference/cli/next"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;dev and build can run concurrently&lt;/a&gt;&#10;.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-follow-on-trap-next-envdts"&gt;The follow-on trap: &lt;code&gt;next-env.d.ts&lt;/code&gt; &lt;/h2&gt;&#10;&lt;p&gt;Next rewrites &lt;code&gt;next-env.d.ts&lt;/code&gt; on every dev start and every build, and the file names the types of whichever directory ran last:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;/// &amp;lt;reference path=&amp;#34;./.next-dev/types/routes.d.ts&amp;#34; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After every &lt;code&gt;next build&lt;/code&gt;, it says &lt;code&gt;./.next/types/routes.d.ts&lt;/code&gt; instead. The file was tracked, so it showed up as modified in later sessions, and the agents kept &amp;ldquo;fixing&amp;rdquo; it with &lt;code&gt;git show HEAD:apps/web/next-env.d.ts &amp;gt; apps/web/next-env.d.ts&lt;/code&gt;. Do what the &lt;code&gt;.gitignore&lt;/code&gt; from &lt;code&gt;create-next-app&lt;/code&gt; does and ignore the file:&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;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;next-env.d.ts&amp;#39;&lt;/span&gt; &amp;gt;&amp;gt; apps/web/.gitignore&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git rm --cached apps/web/next-env.d.ts&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="why"&gt;Why &lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;next dev&lt;/code&gt; and &lt;code&gt;next build&lt;/code&gt; both default to &lt;code&gt;.next&lt;/code&gt; in Next.js 15, and they write different chunk graphs into it. A second &lt;code&gt;next dev&lt;/code&gt; is no better: two webpack compilers write into one directory, and neither knows about the other. The runtime that is already loaded &lt;code&gt;require()&lt;/code&gt;s a chunk by the name from its own graph, gets a file the other compilation wrote, and fails at &lt;code&gt;chunk.ids.length&lt;/code&gt;. The error says &lt;code&gt;length&lt;/code&gt;, so you go looking for an array in your code.&lt;/p&gt;&#10;&lt;p&gt;Coding agents check their work with &lt;code&gt;next build&lt;/code&gt;, usually while your dev server is still running, so you&amp;rsquo;ll hit this more often than you&amp;rsquo;d think.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;An error from inside &lt;code&gt;installChunk&lt;/code&gt; points at the build directory, so look for a second Next process before you read your own code. Never let two Next processes share a &lt;code&gt;distDir&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;If an agent or a script runs &lt;code&gt;next build&lt;/code&gt; in the checkout where your dev server runs, give dev its own &lt;code&gt;distDir&lt;/code&gt; today.&lt;/p&gt;&#10;</description></item></channel></rss>