# macOS - Developer Environment Pitfalls
A macOS developer machine hides four recurring traps: Homebrew Node upgrades break native addons, both macOS secret stores expose secret material through a single flag, Docker on macOS always runs in a VM, and flag-based file moves are not portable. Each trap has a known repair.
## Native addons after a Node upgrade
- Every Homebrew Node major upgrade changes the Node ABI. A native addon such as `better-sqlite3` breaks because its compiled `.node` binary no longer matches the new runtime.
- `npm install -g` cannot repair the binding when a security wrapper blocks install scripts. The blocked install script never compiles the `.node` binary, so reinstalling the package reproduces the breakage.
- The direct repair is `node-gyp rebuild` inside the global package directory: `/opt/homebrew/lib/node_modules/<pkg>/node_modules/<addon>`. A shell wrapper can run this repair automatically on the next failure.
- Bare `npx <pkg>` reuses an existing broken global install from `/opt/homebrew/lib/node_modules`. An explicit `--package=<pkg>@latest` plus a clean `npm_config_cache` forces an isolated install and proves that the package itself works.
## Secrets on the command line
- `security find-generic-password` without `-w` prints only unencrypted metadata: service, account, and timestamps. Use this form for audits and CI existence checks.
- The `-w` flag materializes the secret to stdout. Confine `-w` to the runtime code path and never let it reach logs.
- `op read "op://<vault>/<item>/credential" --account=<account> --no-newline` is the 1Password CLI pattern that injects a key into a wrapper without leaving it in shell history.
## Container runtimes
- The Docker VM backend decides what works.
- Docker Desktop virtualizes `/proc` in a way that breaks libraries that generate an ELF object via `memfd_create()` and load it through `/proc/self/fd/<fd>`. A USDT probe provider based on `libstapsdt` fails this way, and `--pid=host` does not fix it. Such workloads need native Linux or a Lima VM.
- On a Colima setup without Docker.app, the `docker compose` plugin symlink is dead. Use the Homebrew `docker-compose` binary instead. Run `docker context show` before you write "Docker Desktop" in any script or note.
- With Colima, testcontainers silently skips container tests when `DOCKER_HOST` is unset, because it falls back to `unix:///var/run/docker.sock`. Point `DOCKER_HOST` at the Colima socket to run the tests. Remaining errors of the form `error while creating mount source path '.../.colima/default/docker.sock': operation not supported` are a limitation of the local VM, not a test bug. Delegate those runs to CI.
## Atomic no-clobber file moves
- `os.link` plus `os.unlink` is the atomic no-clobber move pattern from the Maildir protocol. `os.link` raises `FileExistsError` atomically when the destination exists and `OSError(EXDEV)` across filesystems.
- `shutil.move` silently degrades to copy plus delete across filesystems. That path is not atomic and it deletes the source, which violates a no-deletion constraint.
- The exit codes of `mv -n` are platform-dependent. The GNU coreutils maintainer noted that it "silently succeeds too long now to change." The portable no-clobber primitive is the link+unlink pair, not a flag.
## How to apply
1. When a CLI with a native addon fails after a Node upgrade, run `node-gyp rebuild` inside `/opt/homebrew/lib/node_modules/<pkg>/node_modules/<addon>`. Do not retry `npm install -g` while a script-blocking policy is active.
2. To prove that a package itself works, run it with `npx --package=<pkg>@latest <command>` and a clean `npm_config_cache`. Do not trust bare `npx <pkg>`, which reuses the broken global install.
3. Print secret metadata only: `security find-generic-password -s <service>` without `-w`. Put every `-w` or `op read` call inside the wrapper or script that needs the secret.
4. Before you debug any container failure on macOS, run `docker context show`. With Colima, export `DOCKER_HOST` before you run testcontainers. Treat mount-source errors as a local VM limitation and delegate those runs to CI.
5. Implement no-clobber moves as `os.link` followed by `os.unlink`. Handle `FileExistsError` as the collision case and `OSError` with `EXDEV` as the cross-device case.
## Observations
- [tooling] A Node major upgrade breaks native addons because the ABI changes, and a blocked install script prevents repair by reinstall. #node (source: agent sessions, 2026-07)
- [security] Both macOS secret stores separate a safe metadata-only mode from a secret-materializing flag. Keep the flag out of logs and history. #secrets (source: agent sessions, 2026-07)
- [containers] The Docker VM backend decides what works. Docker Desktop virtualizes `/proc` and breaks `memfd_create`-based ELF loading. #docker (source: agent sessions, 2026-01)
- [python] The portable no-clobber move is link-then-unlink. `mv -n` exit codes are not reliable across platforms. #python (source: agent sessions, 2026-06)
## Relations
- relates_to [[resources/documentation/Shell - BSD and GNU Tools on macOS]] (BSD and GNU tool behavior decides flag reliability)
- relates_to [[resources/documentation/macOS - Managing Preferences with defaults and PlistBuddy]] (macOS system CLI tools)
- relates_to [[resources/documentation/Go/Instrumentation/Go Instrumentation - Flight Recording and USDT]] (USDT probes and the Docker Desktop `/proc` limit)
## Sources
- Personal agent-session notes, January 2026 to October 2026.