Skip to content

fix(macos): read AI tool versions from package.json to avoid Gatekeeper malware prompt#167

Open
swarit-stepsecurity wants to merge 1 commit into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/version-file-detection
Open

fix(macos): read AI tool versions from package.json to avoid Gatekeeper malware prompt#167
swarit-stepsecurity wants to merge 1 commit into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/version-file-detection

Conversation

@swarit-stepsecurity

@swarit-stepsecurity swarit-stepsecurity commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

Users on macOS see a Gatekeeper dialog during a scan:

"merkle-tree-napi.darwin-arm64.node" Not Opened — Apple could not verify "merkle-tree-napi.darwin-arm64.node" is free of malware that may harm your Mac or compromise your privacy.

merkle-tree-napi.darwin-arm64.node is a native Node addon (a Mach-O dylib) bundled as a transitive dependency of one of the AI CLIs on the machine. A security tool triggering a malware warning is exactly the wrong impression.

Root cause

The AI CLI / agent / framework detectors read each tool's version by executing it with --version (aicli.go getVersion, agent.go getVersion, framework.go getVersion). These AI CLIs are npm-installed Node programs. Launching one boots Node, which dlopens the tool's native .node addons during module init — before argv is even parsed, so the flag is irrelevant. When a quarantined addon (com.apple.quarantine, never cleared after npm download) is loaded, Gatekeeper on recent macOS assesses it and blocks with the popup.

Merely reading a file's bytes never triggers this; only loading/executing does.

Fix

Move version detection to file-based (static-first): read the version from the npm package.json when the binary resolves into a node_modules tree, and only fall back to executing --version for standalone (non-npm) binaries (which are typically notarized, so no prompt).

  • package.json's version is npm's required, canonical field (semver) — authoritative, not a heuristic. Verified it matches --version output for the affected tools (e.g. claude: package.json 2.1.209 == claude --version 2.1.209 (Claude Code)).
  • Mirrors the existing static-first approach for IDEs in resolveDarwinVersion (which was added to dodge an analogous exec hazard — the "Coveo incident").
  • Reuses the node_modules root resolution already in resolveInstallPath (factored into shared nodePackageRoot / resolveRealPath helpers) and the existing readJSONVersion helper. Handles both the Unix symlink layout and the Windows .cmd/.ps1 shim layout.

detectClaudeCowork and detectLMStudioApp already read versions from Info.plist/registry, so they were already safe and are unchanged.

Tests

  • TestVersionFromPackageJSON — unit coverage of the shared helper (npm package w/ and w/o package.json, standalone binary, empty path).
  • TestAICLIDetector_VersionFromPackageJSON / TestAgentDetector_VersionFromPackageJSON — end-to-end: the detected version comes from package.json and the exec fallback (stubbed with a deliberately different value) does not run.
  • Existing install-path tests unchanged and passing (resolveInstallPath behavior preserved).

go build ./..., go vet ./internal/detector/, and go test ./... all green.

…er prompt

The AI CLI/agent/framework detectors read each tool's version by executing
it with --version. On macOS, launching a Node-based CLI boots Node, which
dlopen's the tool's native .node addons during module init (before argv is
even parsed); a quarantined addon such as merkle-tree-napi.darwin-arm64.node
makes Gatekeeper pop an "Apple could not verify ... is free of malware"
prompt at the end user — a security tool triggering a malware warning.

Resolve the version statically from the npm package.json (npm's canonical,
required version field) when the binary resolves into a node_modules tree,
falling back to --version only for standalone (non-npm) binaries, which are
typically notarized. Mirrors the static-first version resolution already
used for IDEs in resolveDarwinVersion. Factors the node_modules root
resolution out of resolveInstallPath into shared helpers.
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.

1 participant