TypeScript

Fix gyp: No Xcode or CLT version detected! in npm install

Learn which of the three checks node-gyp runs on macOS is failing, repair the Command Line Tools, and get native modules compiling again.

Mahdi Ben RhoumaMahdi Ben Rhouma
9 min read

TL;DR#

On macOS, node-gyp asks for an Xcode version before it compiles anything. It tries xcodebuild -version, then the Command Line Tools (CLT) package receipts through pkgutil, then softwareupdate --history. When all three come back empty, it stops with gyp: No Xcode or CLT version detected!. Install the CLT with xcode-select --install, or reset or reinstall a broken copy, confirm with pkgutil --pkg-info=com.apple.pkg.CLTools_Executables, and rerun npm install. The third lookup only arrived during the node-gyp 7 line, and npm 6 never bundled it.

The error#

The Stack Overflow question (396 votes) appeared straight after an upgrade to macOS Catalina 10.15.3. The asker ran Node 8.8.0 and npm 6.11.3, and npm install failed while building fsevents:

text
> fsevents@1.2.11 install /Users/synapse/Documents/synapsefi-dev-ui/node_modules/watchpack/node_modules/fsevents
> node-gyp rebuild
 
No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'.
 
No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'.
 
No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'.
 
gyp: No Xcode or CLT version detected!
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! System Darwin 19.3.0
gyp ERR! node -v v8.8.0
gyp ERR! node-gyp -v v5.0.3
gyp ERR! not ok
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.11 (node_modules/watchpack/node_modules/fsevents):

The three No receipt lines are the useful part. They show exactly which lookups failed, and in what order.

Why it happens#

npm compiles a package with node-gyp when the package's install lifecycle runs node-gyp rebuild, either because the package says so or because npm supplies it as a default. The npm scripts documentation says that if a package has a binding.gyp at its root and no install or preinstall script of its own, npm "will default the install command to compile using node-gyp via node-gyp rebuild". The npm rebuild documentation adds that "gypfile": false also suppresses that default. The fsevents release in the log sets that script explicitly and is restricted to macOS (darwin), as its registry metadata shows:

text
$ npm view fsevents@1.2.11 scripts.install os
scripts.install = 'node-gyp rebuild'
os = 'darwin'

node-gyp rebuild runs configure first. In lib/configure.js, node-gyp finds a Python interpreter and then spawns its vendored copy of gyp with it. If gyp exits non-zero, configure throws `gyp` failed with exit code: 1. Python was therefore already found when you see this error. A missing Python fails earlier, with Could not find any Python installation to use from lib/find-python.js.

On macOS, gyp's Xcode emulation layer needs a toolchain version. The function that supplies it is XcodeVersion() in gyp/pylib/gyp/xcode_emulation.py. It tries three sources in order:

  1. xcodebuild -version. This works only with the full Xcode app. Apple's Command Line Tools documentation states that xcodebuild ships only with Xcode and is not in the Command Line Tools package. On a Mac with only the CLT, this call fails. The source comment there reads "Xcode not installed so look for XCode Command Line Tools". node-gyp also falls through when xcodebuild prints fewer than two lines.
  2. pkgutil receipts. CLTVersion() runs /usr/sbin/pkgutil --pkg-info for com.apple.pkg.CLTools_Executables, then com.apple.pkg.DeveloperToolsCLILeo, then com.apple.pkg.DeveloperToolsCLI. That is the order of the three lines in the log. node-gyp does not capture pkgutil's stderr, so it reaches your terminal. The No receipt for '…' found at '/'. wording is pkgutil's own: Apple's page shows pkgutil printing the same message in its example of deleting a package receipt.
  3. softwareupdate --history. As a last resort, node-gyp searches the update history for a Command Line Tools for Xcode <version> entry.

If none of the three returns a version, gyp raises No Xcode or CLT version detected! and exits with code 1.

The third lookup is the Catalina fix. It arrived through gyp-next commit 208f919, "gyp: fix XCode CLT version detection on Catalina". The gyp-next changelog lists it as "Fixed XCode CLT version detection on macOS Catalina". node-gyp picked it up partway through its 7 line. The first 7.x tag does not contain the fallback, and the next minor tag does, after the node-gyp changelog records an update of the vendored gyp:

text
$ curl -sL https://raw.githubusercontent.com/nodejs/node-gyp/v7.0.0/gyp/pylib/gyp/xcode_emulation.py | grep -c softwareupdate
0
$ curl -sL https://raw.githubusercontent.com/nodejs/node-gyp/v7.1.0/gyp/pylib/gyp/xcode_emulation.py | grep -c softwareupdate
1

node-gyp's archived Catalina note, removed from the repository in v10, has a "node-gyp v7" section saying that release "should solve this problem".

The asker's log reports node-gyp -v v5.0.3, which is what npm 6.11.3 bundled. The npm 6 line never moved past node-gyp 5, so anyone on npm 6 gets only the first two lookups. The registry shows the ranges:

text
$ npm view npm@6.11.3 dependencies.node-gyp
^5.0.3
$ npm view npm@6.14.18 dependencies.node-gyp
^5.1.1

Why does the receipt disappear in the first place? The Catalina note says that "both upgrading to macOS Catalina and running a Software Update in Catalina may cause normal node-gyp installations to fail". Apple says that after a macOS upgrade "the version of the installed package may be incompatible with the new macOS". Neither source documents why pkgutil loses the receipt, so the fixes below treat the CLT install as broken and repair it.

Fix#

1. See what node-gyp sees#

Print the active developer directory, then repeat node-gyp's three lookups in the order it runs them:

bash
xcode-select --print-path
xcodebuild -version
pkgutil --pkg-info=com.apple.pkg.CLTools_Executables
softwareupdate --history | grep "Command Line Tools"

node-gyp itself never calls xcode-select. --print-path is there because it shows the active developer directory (Apple TN2339), and a comment in XcodeVersion() cites "a bogus path set via xcode-select" on old OS X releases as an example of xcodebuild exiting 0 without the expected output. The pkgutil line checks the first of the three package IDs node-gyp tries. A healthy CLT install answers pkgutil with a package-id and a version line. If xcodebuild -version prints an Xcode version and a build number, node-gyp never reaches the receipt check.

2. Install the Command Line Tools if they are missing#

The node-gyp README lists the CLT as the macOS requirement and installs them standalone:

bash
xcode-select --install

Apple's documentation shows the response xcode-select: note: install requested for command line developer tools. A system dialog follows: click Install and accept the licence. The package lands in /Library/Developer/CommandLineTools.

3. Reset the developer directory when the tools are "already installed"#

If step 2 answers xcode-select: error: command line tools are already installed, use "Software Update" to install updates, take the message at its word first. Apple's Command Line Tools documentation says that after a macOS upgrade you should "check for new releases of the command-line tool package using Software Update or the softwareupdate command". List pending updates with the command the Catalina note runs once an install has finished:

bash
softwareupdate -l

Install any Command Line Tools update it lists through Software Update. The Catalina note skips this check when xcode-select --install reports the tools as already installed, and sends you straight to removing and reinstalling them (step 4 below). The accepted answer tries a reset first, because the reinstall "requires large redownload". The reset step, which both sources use, is:

bash
sudo xcode-select --reset
xcode-select --print-path

The note says the reset itself prints nothing ("No output is normal"); --print-path should then print a directory. Only switch the developer directory if --print-path still points at a folder that no longer exists, or at an Xcode.app you no longer use:

bash
sudo xcode-select --switch /Library/Developer/CommandLineTools

Apple's TN2339 documents xcode-select -switch for choosing the developer directory. If --print-path already prints /Library/Developer/CommandLineTools, switching to the same path changes nothing. In that case skip it and go to step 4.

4. Remove and reinstall the Command Line Tools#

When the receipt is still missing, reinstall. Apple documents this exact removal command:

bash
sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install

Use the explicit path, not the sudo rm -rf $(xcode-select --print-path) form from the accepted answer. In the second answer on the question, --print-path returned /Applications/Xcode.app/Contents/Developer. On that machine the substituted form deletes a folder inside the Xcode app bundle, not the Command Line Tools.

5. Full Xcode users: accept the licence#

If you rely on the full Xcode app, the archived note's Xcode path runs this step ("If you agree with the licensing terms"):

bash
sudo xcodebuild -license accept
xcodebuild -version

Once xcodebuild -version prints its two lines, node-gyp takes the version from there and skips the receipt lookup.

6. Run the install again#

bash
npm install

This repeats the step that failed. In the asker's log, npm skipped fsevents as a failed optional dependency, so rerunning the install is what retries it. For a required native package that is still in node_modules, you can rebuild just that package instead:

bash
# PKG_NAME = the package on the "install" line of your log
npm rebuild "$PKG_NAME"

According to the npm rebuild documentation, the command runs the install lifecycle scripts again, and a binding.gyp package without its own install or preinstall script gets node-gyp rebuild by default. Take the package name from the install line just above node-gyp rebuild in your log.

Verify the fix#

First check that the receipt is back. Apple's page shows the expected shape, using a 26.x beta of the Command Line Tools as its example:

bash
pkgutil --pkg-info=com.apple.pkg.CLTools_Executables
text
package-id: com.apple.pkg.CLTools_Executables
version: 26.1.0.0.1.1760670222
volume: /
location: /
install-time: 1761350460

Your version string will differ. Any version: line means node-gyp's second lookup now succeeds. node-gyp's acid test script is still in the repository and runs the same three pkgutil queries, printing Command Line Tools version: … or Command Line Tools not found.

Next, check that the build really happened, not merely that the error went away. The node-gyp README says compiled bindings "end up in build/Debug/ or build/Release/":

bash
ls node_modules/fsevents/build/Release/*.node

Adjust the path to wherever the package sits; in the asker's tree it was nested under node_modules/watchpack/node_modules/fsevents. If the error appears again, read the gyp ERR! node-gyp -v line. It names the node-gyp version npm actually used, which tells you whether the softwareupdate fallback was even available.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Old Node and npm: when upgrading node-gyp is not an option#

The archived Catalina note offered a workaround for old npm: install a newer node-gyp yourself and point npm at it with npm config set node_gyp <path to node-gyp>. For the asker's setup that route leaves the supported range. The first node-gyp release with the softwareupdate fallback declares a minimum Node version above 8 in its manifest:

text
$ npm view node-gyp@7.1.0 engines.node
>= 10.12.0

On Node 8, repairing the Command Line Tools (steps 2 to 6) is the supported fix.

A recent toolchain already has the fallback. The Node.js release index shows every Node 24 LTS release shipping npm 11, and npm's node-gyp config defaults to "the node-gyp bin that ships with npm". The registry shows what a recent npm 11 release bundles, and node-gyp 12 still contains the softwareupdate lookup, as does the main branch:

text
$ npm view npm@11.19.0 dependencies.node-gyp
^12.4.0
$ curl -sL https://raw.githubusercontent.com/nodejs/node-gyp/v12.4.0/gyp/pylib/gyp/xcode_emulation.py | grep -c softwareupdate
1
$ curl -sL https://raw.githubusercontent.com/nodejs/node-gyp/main/gyp/pylib/gyp/xcode_emulation.py | grep -c softwareupdate
1

To move off an old Node, the Node version guide for Next.js 15 covers upgrading with nvm or Volta. It also covers deleting node_modules and the lockfile after the switch, because native modules built for the old runtime can break.

The fsevents case: an optional dependency failing#

The asker's log ends with npm WARN optional SKIPPING OPTIONAL DEPENDENCY. npm kept going. The package.json reference says optional dependencies are for when "you would like npm to proceed if it cannot be found or fails to install". The npm 11 docs also let you skip them entirely with --omit=optional, as the config reference documents. The docs make no promise about what a dependent package does without them. A required native dependency (anything not listed as optional) fails the whole install on the same Xcode check, so repairing the CLT is still the real fix.

The same log contains npm WARN synapsefi-dev-ui@2.0.20 No repository field., which has nothing to do with node-gyp. npm 6 prints it when a package.json lacks a repository field. The npm repository-field warning page explains it, and for an app you never publish, "private": true silences it.

The next error after the Command Line Tools are fixed#

The documented one is Python. The README carries this warning:

text
$ curl -sL https://raw.githubusercontent.com/nodejs/node-gyp/main/README.md | grep "Python >=" | cut -c3-
Python >= v3.12 requires `node-gyp` >= v10

A Mac that gains a newer Python while the project is pinned to an old node-gyp therefore trades this error for a Python one. The README's Python section shows how to point node-gyp at a specific interpreter:

bash
node-gyp rebuild --python /path/to/executable/python
export npm_config_python=/path/to/executable/python

Past configure, the CLT supply clang, clang++ and make (README), so most later failures are compiler errors in the package's own C or C++ source.

The Xcode check belongs to the macOS build path, so Linux CI and Docker builds fail differently. In gyp's Makefile generator, the Xcode settings object is only created when the build flavour is mac, and the README lists separate Unix requirements (Python, make and a compiler such as GCC). The README's command table also says configure generates build files for the platform it runs on, so an addon compiled by the macOS toolchain belongs to the Mac. If the same project runs in a container, the Docker Compose development setup keeps node_modules in a named volume so the host copy does not clobber the container's.

A brute-force variant circulates in the third answer on the question. It creates /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress so that softwareupdate -l lists the Command Line Tools. Apple's documentation does not mention that file. Try the documented route first: Software Update, then xcode-select --install after removing /Library/Developer/CommandLineTools.

gyp: No Xcode or CLT version detected! is never about your project's code. It means one function in node-gyp asked three questions about the toolchain on your Mac and got no answer to any of them. The No receipt lines and the node-gyp -v line in the same log show which questions failed and which node-gyp asked them. Fix the Command Line Tools until pkgutil returns a version, and the build moves on to compiling.

Further reading: Ineffective mark-compacts near heap limit: npm build fix. Also see Fix dotenv: command not found in a NestJS start script. Also see Fix print "%s.%s.%s" invalid syntax in yarn install. Also see Fix ERR_REQUIRE_ESM in gulp: ESM-only plugins, old gulp-cli.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote