Skip to content

fix(npm): merge NODE_OPTIONS and drop quotes in shadow --node-options - #1433

Open
John-David Dalton (jdalton) wants to merge 1 commit into
v1.xfrom
jdalton/fix-npm-node-options-1160-1036
Open

fix(npm): merge NODE_OPTIONS and drop quotes in shadow --node-options#1433
John-David Dalton (jdalton) wants to merge 1 commit into
v1.xfrom
jdalton/fix-npm-node-options-1160-1036

Conversation

@jdalton

@jdalton John-David Dalton (jdalton) commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Two things go wrong today when you run socket npm run build on Node 24 or newer.

It crashes. Node throws TypeError [ERR_MISSING_OPTION]: --permission is required and the build dies. Nothing you did caused it — socket npm mangles its own flags on the way in.

It silently discards your NODE_OPTIONS. If you had NODE_OPTIONS=--max-old-space-size=4096 set globally, running your build through socket npm throws it away. Builds that need the extra heap just fail, with no message saying why.

Both come from one line in src/shadow/npm-base.mts, and this PR fixes both. Fixes #1160 and #1036.

Bug 1 — the quotes were never needed, and Next.js chokes on them (#1160)

The old line wrapped the value in literal single quotes:

`--node-options='${nodeOptionsArg ? nodeOptionsArg.slice(15) : ''}${cmdFlagsToString(permArgs)}'`

shadowNpmBase spawns npm without a shell, so nothing ever interprets those quotes. They become part of the value npm assigns to NODE_OPTIONS for lifecycle scripts:

NODE_OPTIONS='--permission --allow-child-process --allow-fs-read=* ...'

Node itself tolerates the garbled tokens. Consumers that re-tokenize NODE_OPTIONS on whitespace and only honour " do not — Next.js's TypeScript build worker is the one users hit. It drops '--permission (it has a leading quote glued to it) while keeping the valid --allow-* flags. Node 24 then throws, because the allow-list flags are orphaned:

TypeError [ERR_MISSING_OPTION]: --permission is required

Removing the quotes keeps --permission a clean, standalone token.

Bug 2 — npm's --node-options replaces the inherited value, so ours clobbered the user's (#1036)

The old value merged in a --node-options= npm argument if one was present, but never the caller's existing process.env.NODE_OPTIONS. Since npm's --node-options config replaces the inherited NODE_OPTIONS for scripts, a globally-configured NODE_OPTIONS was silently dropped:

$ NODE_OPTIONS=test socket npm run echo   # echoes socket's flags, not "test"

The value now merges three sources, in this order:

Order Source
1 process.env.NODE_OPTIONS — whatever the caller already had
2 any --node-options= npm argument, with its prefix stripped
3 Socket's own permission flags
The change — one pure exported helper, so both behaviours are testable without spawning

The construction moved out of the template literal and into a pure, exported buildNpmNodeOptionsArg(envNodeOptions, nodeOptionsArg, permArgs), and the call site now uses it. Empty and falsy inputs are filtered out so the merge never leaves a stray separator.

Concretely, for NODE_OPTIONS=--max-old-space-size=4096 socket npm run build on Node 24:

  • Before: --node-options='--permission --allow-child-process ...'--permission dropped downstream → crash, and --max-old-space-size lost.
  • After: --node-options=--max-old-space-size=4096 --permission --allow-child-process --allow-fs-read=* ... → all tokens clean, user value preserved.
What I checked — 7 new unit tests, plus typecheck and lint on the changed files

New src/shadow/npm-base.test.mts covers:

Ran:

  • pnpm exec vitest run src/shadow/npm-base.test.mts — 7/7 pass
  • pnpm run check:tsc — clean
  • oxlint on changed files — 0 errors (only pre-existing no-named-as-default-member warnings on unchanged lines)
Branch scope — this targets v1.x; main does not have the bug

Targets v1.x, the branch where the shadow-npm wrapper lives. The issues reproduce on v1.1.78.

On main, socket npm hands off to Socket Firewall and no longer builds this argument, so main is unaffected and needs no companion PR.


Note

Medium Risk
Changes NODE_OPTIONS passed into npm lifecycle scripts (build/run), which can affect all downstream script processes, but the logic is isolated, well-tested, and targets known production bugs.

Overview
Fixes how socket npm builds the --node-options flag for npm lifecycle scripts by replacing inline string templating with buildNpmNodeOptionsArg.

The value is no longer wrapped in single quotes, so --permission stays a real token when tools re-split NODE_OPTIONS on whitespace (fixes Node ≥ 24 ERR_MISSING_OPTION on builds like Next.js, #1160). The helper also merges process.env.NODE_OPTIONS, any existing npm --node-options=… arg, and Socket’s permission flags in that order, because npm’s flag replaces inherited NODE_OPTIONS for scripts (#1036).

Adds npm-base.test.mts with seven unit tests for quoting, merge order, prefix stripping, and empty inputs.

Reviewed by Cursor Bugbot for commit 3f272fb. Configure here.

The shadow npm wrapper built npm's `--node-options` value as:

    --node-options='<user value><perm flags>'

Two bugs on that one line:

1. Node >= 24 crash (#1160). spawn() runs npm without a shell, so the
   literal single quotes became part of the NODE_OPTIONS value npm set
   for lifecycle scripts. Consumers that re-tokenize NODE_OPTIONS on
   whitespace (e.g. Next.js) only understand `"`, not `'`, so they
   dropped `'--permission` (leading quote) while keeping the `--allow-*`
   flags. Node 24 then throws `ERR_MISSING_OPTION: --permission is
   required` because the allow-list flags are orphaned. Dropping the
   quotes keeps `--permission` a clean, standalone token.

2. NODE_OPTIONS override (#1036). The value only folded in a
   `--node-options=` npm arg, never the caller's existing
   `process.env.NODE_OPTIONS`, so npm replaced the user's global
   NODE_OPTIONS instead of extending it. We now merge, in order,
   process.env.NODE_OPTIONS, any `--node-options=` npm arg, and our
   permission flags.

Extracted the construction into a pure, exported `buildNpmNodeOptionsArg`
helper and unit-tested both fixes.
@jdalton
John-David Dalton (jdalton) force-pushed the jdalton/fix-npm-node-options-1160-1036 branch from 3f272fb to 7fdbb3c Compare July 30, 2026 15:14
@jdalton
John-David Dalton (jdalton) enabled auto-merge (squash) July 30, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant