Skip to content

fix(platform): stop macOS Gatekeeper from blocking the nodeup binary - #133

Merged
dipto0321 merged 1 commit into
mainfrom
fix/platform/gatekeeper-quarantine
Nov 1, 2026
Merged

dipto0321 merged 1 commit into
mainfrom
fix/platform/gatekeeper-quarantine

Conversation

@dipto0321

Copy link
Copy Markdown
Owner

Summary

macOS could install a nodeup that could not be executed at all.
The command produced no output and no error — it just died.

macOS marks files downloaded from the internet with the
com.apple.quarantine extended attribute, and Gatekeeper refuses to
execute an unsigned binary that still carries it. nodeup ships as
an unsigned Go binary (no Apple Developer ID certificate), so this was
fatal for it. The kernel killed the process before main() ever ran:

ASP: Security policy would not allow process: 82275, /opt/homebrew/Cellar/nodeup/1.2.1/bin/nodeup

Homebrew quarantines every artifact it downloads and propagates the
attribute onto the files it extracts, so brew install dipto0321/tap/nodeup produced a nodeup that could not be run.

The repair has to happen out of band, at install time — a
quarantined executable is killed before its first instruction, so no
amount of self-repair code inside the binary can strip the attribute.
Both download channels now do it as part of the install:

  • Homebrew cask — a postflight_steps hook runs
    xattr -dr com.apple.quarantine on the staged path, so the
    attribute is gone before the binary is linked into the prefix. This
    is the remedy Homebrew's own cask guidance recommends (and what
    GoReleaser's cask docs point at).

  • npm wrapper — nodeup-npm/scripts/install.js strips the
    attribute from the extracted binary before it is moved into place.

Both paths are best-effort: an install never fails because the
attribute could not be removed, and both are no-ops on platforms that
do not have the attribute.

Verification

End-to-end against the exact cask GoReleaser renders, installed
with a real brew install --cask from a scratch tap:

Install path com.apple.quarantine Runs
Cask with the postflight_steps hook absent 3/3
Control cask without the hook present 0/3
Control formula (bin.install — what the tap shipped) present 0/3

The control formula reproduces the reported symptom exactly.

Ad-hoc codesigning was tested and rejected as a remedy: codesign --force --sign - leaves spctl --assess at rejected and the binary
still fails 0/3.

Also confirmed that the Go-level self-heal is impossible: a quarantined
binary times out and never creates the marker file that main()
writes — the kernel kills it before the first instruction.

Changes

  • .goreleaser.yaml — homebrew_casks[].hooks.post.install_steps
    strips the quarantine attribute on macOS.
  • nodeup-npm/scripts/install.js — new stripQuarantine() helper,
    called after the binary is moved into place.
  • nodeup-npm/scripts/install_test.js — 4 new tests covering the
    platform gate, the probe-then-strip sequence, the recursive form, and
    the "a failed strip must not abort the install" guarantee.
  • docs/installation.md — a new macOS Gatekeeper section: what
    the failure looks like, how each channel handles it, and the manual
    recovery one-liner (xattr -dr com.apple.quarantine) for
    browser-downloaded tarballs (tar/zip extraction propagates the
    attribute too).
  • CHANGELOG.md — the v1.2.2 Fixed entry.

Test plan

  • go build ./...
  • go vet ./...
  • gofmt -l . (clean)
  • golangci-lint run ./... → 0 issues
  • go test -race ./... → all packages ok
  • node nodeup-npm/scripts/install_test.js → 25 tests, 0 issues
  • goreleaser check → configuration valid
  • goreleaser release --snapshot → renders the cask with the
    postflight_steps hook; the rendered file is valid Ruby
  • Real brew install --cask of the rendered cask → binary has no
    quarantine attribute, runs 3/3

🤖 Generated with puku-cli

Closes #132

macOS marks downloaded files with the `com.apple.quarantine` extended
attribute, and refuses to execute an unsigned binary that still carries
it. The kernel kills the process before `main()` ever runs:

    ASP: Security policy would not allow process: 82275, /opt/homebrew/Cellar/nodeup/1.2.1/bin/nodeup

`nodeup` ships as an unsigned Go binary (no Apple Developer ID
certificate), so this was fatal for it — the command produced no output
and no error, it just died. Homebrew quarantines every artifact it
downloads and propagates the attribute to the files it extracts, so
`brew install dipto0321/tap/nodeup` produced a `nodeup` that could not
be executed.

The repair has to happen out of band, at install time: a quarantined
executable is killed before its first instruction, so no amount of
self-repair code inside the binary can strip the attribute. Both
download channels now strip it as part of the install:

- The Homebrew cask carries a `postflight_steps` hook that runs
  `xattr -dr com.apple.quarantine` on the staged path, so the
  attribute is gone before the binary is linked into the prefix. This
  is the remedy Homebrew's own cask guidance recommends.

- The npm wrapper's `install.js` strips the attribute from the
  extracted binary before it is moved into place.

Both paths are best-effort — an install never fails because the
attribute could not be removed — and both are no-ops on platforms
without the attribute.

Verified end-to-end against the exact cask GoReleaser renders,
installed with a real `brew install --cask`: the binary comes out with
no quarantine attribute and runs 3/3 times. The control (a formula
install, which is what the tap shipped before) reproduces the original
symptom exactly: quarantine present, 0/3 runs.

Closes #132.
@cocogitto-bot

cocogitto-bot Bot commented Nov 1, 2026

Copy link
Copy Markdown

✔️ 8b109f1 - Conventional commits check succeeded.

@dipto0321
dipto0321 merged commit 2f185bc into main Nov 1, 2026
11 checks passed
@dipto0321
dipto0321 deleted the fix/platform/gatekeeper-quarantine branch November 1, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS Gatekeeper blocks nodeup from running at all — the installed binary is quarantined and the kernel kills it before main()

1 participant