macOS: recurring Gatekeeper "ripgrep.node could not be verified" when `claude -p` is spawned by a browser (native-messaging host) — quarantine-strip helper skips linker-signed modules
Summary
On macOS, when the Claude Code CLI is launched by a process descended from a quarantine-enabled app (e.g. a Chrome/Brave native-messaging host), every claude -p invocation triggers a Gatekeeper "'….node' could not be verified free of malware" dialog for the bundled ripgrep.node. It recurs on every run and cannot be dismissed permanently. The CLI already ships a helper meant to prevent this (re-sign + strip quarantine), but it early-returns for linker-signed binaries — and the extracted ripgrep.node is exactly adhoc,linker-signed, so the fix never runs on the one file that needs it.
Environment
- Claude Code v2.1.49 (native install)
- macOS (Darwin 25.x), Apple Silicon
- Launcher: a Node native-messaging host spawned by Brave/Chrome, which spawns
claude -p
Reproduction
- Register a Chrome/Brave native-messaging host that runs
claude -p --output-format json --tools "" "hi". - Trigger it from the extension (so
claudeis a descendant of the browser). - A Gatekeeper dialog appears for a hidden
$TMPDIR/.<16-hex>-00000000.node. - Click "Done" / approve via System Settings → it reappears on the next call.
Runs of the same command from Terminal never prompt, localizing it to quarantine inheritance + the ineffective strip.
Evidence
$ claude --version
2.1.49 (Claude Code)
# a freshly-extracted ripgrep.node during a run:
$ codesign -dvvv <captured>.node
Identifier=ripgrep.node
CodeDirectory v=20400 size=48040 flags=0x20002(adhoc,linker-signed) …
Signature=adhoc
TeamIdentifier=not set
# the CLI's own quarantine-strip helper (strings of the binary; src/utils/ripgrep.ts):
…find((B)=>B.includes("linker-signed")))return; // ← early-return
try{
await WA("codesign",["--sign","-","--force","--preserve-metadata=entitlements,requirements,flags,runtime",R]);
await WA("xattr",["-d","com.apple.quarantine",R]); // ← never reached for linker-signed
}catch(B){r(B)}
Root cause (verified in the v2.1.49 binary)
ripgrep.node(Bun-embedded N-API addon) is extracted to$TMPDIRunder a random filename every run — even with--tools "".- Files created under a browser process tree inherit
com.apple.quarantine; the ad-hoc-signed module is then blocked by Gatekeeper ondlopen. - "Allow Anyway" can never persist: approval is per-file, and each run mints a new filename.
- The bug: the remediation in
src/utils/ripgrep.tschecks…find((B) => B.includes("linker-signed"))) return;before thecodesign --sign - --force …/xattr -d com.apple.quarantinecalls. The module isCodeDirectory … flags=0x20002(adhoc,linker-signed), so the guard fires and the function returns before the quarantine strip it exists to perform.
Impact
Any integration that runs claude -p from a GUI-app-descended process on macOS (browser extensions / native-messaging hosts, some Electron/Tauri wrappers, automation launched from a quarantine-enabled parent) gets an unactionable Gatekeeper popup on every invocation.
Suggested fixes (any one)
- Decouple the two operations: even when
linker-signed(skip the re-sign to avoid disturbing a managed signature), still runxattr -d com.apple.quarantine— that's the part these files need and it's safe. - Notarize / stably re-sign the embedded
ripgrep.node, and/or extract it to a content-addressed stable path so a one-time Gatekeeper approval can persist. - Narrow the guard so it only skips genuinely notarized/Developer-ID signatures, not
adhoc,linker-signedones.
Workaround
Move the claude -p spawn out of the browser's process tree (e.g. a launchd user agent that the browser host relays to over a unix socket). Files created by a launchd-descended process aren't quarantined, so the popup disappears — a heavy per-integration workaround for what looks like a one-line fix in the helper.