skip to content

In a bare React Native iOS project, why can an Xcode build fail to find Node, and how do .xcode.env and .xcode.env.local fix it?

level: middleimportance: should knowfreq 34%

answer

  1. build phases skip your shell profile
  2. NODE_BINARY is the variable
  3. .local overrides the committed file
  4. pod install writes both if missing
  5. stale path after switching Node versions

basics

~20 s

Xcode's build phases run in a non-interactive shell that never loads a version manager like nvm, so node may not be on PATH. React Native's phases read NODE_BINARY from ios/.xcode.env, then from the uncommitted, machine-specific ios/.xcode.env.local.

solid answer

~40 s

Xcode runs script build phases in a plain, non-interactive shell, so anything your `~/.zshrc` sets up, such as nvm, is missing. React Native still needs Node inside the build: the phase that bundles the JavaScript and the Codegen phase both call it. They go through `with-environment.sh`, which defaults `NODE_BINARY` to `command -v node`, then sources `ios/.xcode.env` and finally `ios/.xcode.env.local`, which overrides it. `pod install` creates both files if they are missing: the committed `.xcode.env` exports `$(command -v node)`, and the git-ignored `.xcode.env.local` exports the absolute path of the Node that ran `pod install`. A build that fails with "Could not find node" or runs the wrong Node is fixed by pointing `NODE_BINARY` at the right binary in `.xcode.env.local`.

code

bash · 5 lines
bash
# ios/.xcode.env (committed)
export NODE_BINARY=$(command -v node)

# ios/.xcode.env.local (git-ignored, this machine only)
export NODE_BINARY=$HOME/.nvm/versions/node/v24.3.0/bin/node

go deeper

for a junior

Recall that Xcode builds call Node and read its path from NODE_BINARY in ios/.xcode.env, with .xcode.env.local overriding it on your machine.

for a middle

Explain why script phases miss your shell profile, the order with-environment.sh sources the two files, and which phases actually need Node.

for a senior

Diagnose a build that works in Terminal but fails in Xcode, a stale path after a Node switch, and a committed local file breaking teammates.

for a principal

Decide how the team pins its Node version across laptops and CI so the Xcode build and the engine range never drift apart.

## Why Xcode needs Node at all A bare React Native iOS app is built by Xcode, but part of the build is JavaScript tooling: - the **"Bundle React Native code and images"** build phase runs `react-native-xcode.sh`, which calls Node to produce the JavaScript bundle for builds that embed it; - the **Codegen** script phase uses Node to locate and run `@react-native/codegen`, which generates native code from the app's typed specs. Both need a working `node` executable. ## Why the build cannot see your Node A **script build phase** is a shell script Xcode runs during the build. Xcode starts it in a **non-interactive** shell that does not read your interactive startup files such as `~/.zshrc`. If Node was installed through a **version manager** (nvm, fnm, asdf, Volta and similar), the `PATH` changes that make `node` resolvable usually live in those files, so inside the build phase `node` is simply not found, or a different system Node is found instead. The same app builds from a terminal where your profile is loaded and fails from Xcode's Run button. ## The files React Native uses Since React Native 0.69 the template has used **`.xcode.env`** to decouple the build from the machine's shell. React Native's `with-environment.sh` wrapper, which the build phases call, does this in order: 1. Sets `NODE_BINARY` to the result of `command -v node`, if there is one. 2. Sources `ios/.xcode.env`, which may override it. 3. Sources `ios/.xcode.env.local` if it exists, which overrides both. 4. Runs the wrapped script with the resulting environment. | File | Committed? | Typical content | Purpose | |---|---|---|---| | `ios/.xcode.env` | yes | `export NODE_BINARY=$(command -v node)` | shared default for the team | | `ios/.xcode.env.local` | no, git-ignored | `export NODE_BINARY=/absolute/path/to/node` | one machine's override | `pod install` (through `prepare_react_native_project!`) **creates** both files when they are missing. For `.xcode.env.local` it asks the running Node for its own path, so the file captures the exact binary that ran `pod install`. The template's `.gitignore` excludes `**/.xcode.env.local` because that path is only valid on one machine. ## How it fails, and the fixes - **Codegen phase fails with "Could not find node"**: `NODE_BINARY` is empty after sourcing both files. The error text itself says the `.xcode.env` or `.xcode.env.local` files are misconfigured. Export a valid path. - **The build worked last week and now fails**: you removed or switched a Node version and `.xcode.env.local` still names the old absolute path. Delete the file and rerun `pod install`, or edit the path. - **A teammate's absolute path appears in the repo**: someone committed `.xcode.env.local`. Remove it from git; it must stay machine-local. - **The build uses a Node below React Native's engine range**: 0.87 declares `^22.13.0 || ^24.3.0 || >= 26.0.0`; point `NODE_BINARY` at a supported version. The docs also note an alternative for nvm plus zsh users: move nvm's initialisation from `~/.zshrc` into `~/.zshenv`, which every zsh reads, and make sure the script phases use `/bin/zsh`. ## Beyond Node `.xcode.env` is a general hook, not only a Node path. Any environment variable a script phase needs can be exported there, and a team can keep machine-specific values in `.xcode.env.local`. That keeps build scripts independent of whoever's shell happens to be running Xcode. ## A quick diagnostic routine When an iOS build of a React Native app fails around Node, work through it in this order: 1. Open the failing build phase's log in Xcode's report navigator and look for the line `Node found at:` that `with-environment.sh` prints; it shows which binary the phase actually resolved. 2. Compare that path with `command -v node` in your terminal and with the version the project expects. 3. Inspect `ios/.xcode.env.local`: if it names a path that no longer exists, delete it and rerun `pod install` from a terminal where the right Node is active. 4. Check that `ios/.xcode.env` is committed and still exports `NODE_BINARY` rather than hard-coding a personal path. 5. Rebuild; the Codegen and bundle phases should now find the same Node the rest of your tooling uses. The underlying rule is simple: the committed file describes how the team finds Node, and the local file records where this machine keeps it.

  • Why must .xcode.env.local stay out of git in a React Native repo?
    It holds an absolute path to one machine's Node binary. Committed, it points every other developer's Xcode build at a path that does not exist on their machine, and the bundle or Codegen phase fails. The template's `.gitignore` excludes it for that reason.
  • After switching Node versions with nvm, a React Native iOS build breaks; what is the quickest fix?
    Update `NODE_BINARY` in `ios/.xcode.env.local` to the new binary, or delete the file and rerun `pod install`, which recreates it with the path of the Node that ran it.

The committed .xcode.env is the team's printed recipe card, and .xcode.env.local is the sticky note on your own copy: whoever cooks reads the card first, then the note, and the note wins where they disagree.

saying these in an interview costs you the question

  • Xcode build phases load ~/.zshrc just like Terminal does
  • .xcode.env.local should be committed so the team shares one path
  • The iOS build never runs Node; only Metro does
  • .xcode.env is sourced after .xcode.env.local and overrides it