skip to content

After a macOS major upgrade, a developer's `git` and `cc` commands start failing with `xcrun: error: invalid active developer path`. What are /usr/bin/git and /usr/bin/cc actually doing on macOS, and how do you fix and pin that toolchain?

level: seniorimportance: should knowfreq 46%

answer

  1. /usr/bin holds stubs, not the real tools
  2. one stored path, now pointing at nothing
  3. the error message names the directory it wanted
  4. machine-wide switch versus per-process override
  5. headers moved out of /usr/include years ago

basics

~20 s

The tools in /usr/bin are thin shims that locate the real binaries through xcrun in the active developer directory. A macOS upgrade removes or invalidates that directory, so the shims fail until the Command Line Tools are reinstalled or xcode-select points at a valid path.

solid answer

~40 s

macOS ships stubs, not toolchains. `/usr/bin/git`, `/usr/bin/cc`, `/usr/bin/clang` and `/usr/bin/make` are shims that ask `xcrun` to resolve the real binary inside the **active developer directory** — either `/Library/Developer/CommandLineTools` or an installed Xcode's `Contents/Developer`. A major macOS upgrade removes or invalidates the Command Line Tools, leaving that stored path dangling, and every shim then reports `invalid active developer path`. Fix it by reinstalling with `xcode-select --install`, or by pointing at a valid directory with `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`, or resetting to the default with `sudo xcode-select --reset`. Verify with `xcode-select -p` and `xcrun --find clang`. On build machines, do not rely on the machine-wide setting: export `DEVELOPER_DIR` in the job so the Xcode and SDK are pinned, because a runner-image update can otherwise change your compiler silently.

code

bash · 8 lines
bash
# Diagnose: what is active, and can xcrun resolve through it?
xcode-select -p
xcrun --find clang
xcrun --sdk macosx --show-sdk-path

# Repair: reinstall the standalone tools, or point at a full Xcode.
xcode-select --install
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer

go deeper

for a junior

Know that the fix for an invalid active developer path is normally xcode-select --install, and that macOS needs Apple's Command Line Tools before compilers or Homebrew source builds work at all.

for a middle

Explain the indirection: /usr/bin entries are shims that resolve real binaries via xcrun inside the active developer directory, which xcode-select -p reports and --switch changes.

for a senior

Read the failing path out of the error and treat it as stale state, then remove the fragility on build machines by pinning DEVELOPER_DIR and SDKROOT per job rather than relying on a machine-wide setting.

for a principal

Own toolchain versioning as a supply-chain decision: which Xcode a fleet builds with should be declared, reviewed and rolled forward deliberately, because an image update that shifts the compiler and SDK changes artifacts without failing anything.

## The shims The surprise for anyone arriving from Linux is that macOS does not ship a compiler or a git in the usual sense. What is in `/usr/bin` are small stub programs. When you run `/usr/bin/git`, the stub asks `xcrun` to find the real `git` inside the **active developer directory**, and then executes it. If no developer directory is configured, the stub triggers the graphical "install the command line developer tools?" prompt — and in a non-interactive shell, an SSH session or a CI job, that becomes a hard error instead. The active developer directory is one of two things: - `/Library/Developer/CommandLineTools` — the standalone Command Line Tools package: clang, git, make, headers and the macOS SDK. - `/Applications/Xcode.app/Contents/Developer` — the full Xcode, which adds `xcodebuild`, other platform SDKs and the simulators. `xcode-select` reads and writes which one is active, machine-wide. ## Why an upgrade breaks it A major macOS upgrade replaces system content and commonly removes or supersedes the installed Command Line Tools. The stored active-developer path survives the upgrade, now pointing at something that is gone or incomplete, and the shims report exactly that: ``` xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun ``` The message names the path it tried, which is the useful part. Nothing is corrupt; the pointer is stale. ## The repair sequence ```bash xcode-select -p # what is currently active? xcode-select --install # reinstall the standalone Command Line Tools sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer sudo xcode-select --reset # back to the system default xcrun --find clang # prove resolution works xcrun --sdk macosx --show-sdk-path ``` If full Xcode is the target, two more things routinely block a headless machine: the licence must be accepted (`sudo xcodebuild -license accept`) and first-launch components must be installed (`sudo xcodebuild -runFirstLaunch`). Both fail builds with messages that look nothing like "you forgot to accept a licence", so put them in the provisioning script. ## Where the headers went Since macOS Mojave there is no `/usr/include`. System headers live inside the SDK in the developer directory, and the compiler finds them because `xcrun` hands it the right sysroot. This is why an old autotools build that hardcodes `-I/usr/include` fails on a modern Mac, and why `xcrun --show-sdk-path` and the `SDKROOT` environment variable turn up in build scripts. It is also why Homebrew's `brew doctor` complains loudly when the Command Line Tools are missing: any formula that has to build from source needs that SDK. ## Pinning, which is the senior half of the answer `xcode-select --switch` sets a **machine-wide** default. That is fine for a laptop and wrong for a build fleet, because it is global mutable state that any job or any admin can change under you. The per-process override is the environment variable `DEVELOPER_DIR`: ```bash export DEVELOPER_DIR=/Applications/Xcode_16.app/Contents/Developer xcrun --find clang ``` Every tool that resolves through `xcrun` honours it, so a CI job can select its own Xcode without touching the machine and without racing a parallel job that wants a different one. `SDKROOT` pins the SDK the same way. The failure this prevents is subtle and expensive: a hosted macOS runner image is updated, its default Xcode moves a major version, and your builds silently switch compiler and SDK. Nothing errors; the artifacts change. Pinning `DEVELOPER_DIR` per job turns that into a deliberate, reviewable upgrade instead of a surprise. ## The judgement an interviewer is listening for Three things. First, that you know `/usr/bin` on macOS holds shims and not the tools, so "git is installed" and "git works" are different statements. Second, that you read the error's path and treat it as a stale pointer rather than reinstalling everything blind. Third, that machine-wide toolchain state is a liability on shared build machines and the fix is per-process pinning, not a runbook step telling the next person to run `xcode-select --switch` again.

  • What is the difference between installing the Command Line Tools and installing full Xcode?
    The Command Line Tools package is the standalone toolchain — clang, git, make, headers and the macOS SDK — installed under /Library/Developer/CommandLineTools. Full Xcode adds xcodebuild, the other platform SDKs and simulators, and lives inside the application bundle. For building command-line software and Homebrew formulae from source, the Command Line Tools are enough.
  • Why is DEVELOPER_DIR preferable to xcode-select --switch on a shared build machine?
    `xcode-select --switch` mutates machine-wide state, so concurrent jobs wanting different Xcode versions race each other and an admin change silently alters every build. `DEVELOPER_DIR` is per process: each job declares its own toolchain, the choice is visible in the job definition, and a runner-image update cannot move your compiler without a code change.
  • Why does an old build script that passes -I/usr/include fail on a current Mac?
    Since macOS Mojave there is no /usr/include. System headers live inside the SDK in the active developer directory, and the compiler reaches them because xcrun supplies the right sysroot. Scripts should take the path from `xcrun --show-sdk-path` or SDKROOT rather than assuming the old location.

saying these in an interview costs you the question

  • Thinking macOS ships a normal git and compiler in /usr/bin
  • Reinstalling macOS or Homebrew instead of reading the path in the error
  • Believing xcode-select --switch is per-user or per-shell
  • Assuming /usr/include still exists on a modern Mac
  • Treating a CI runner's default Xcode version as stable

context