Shipping an npm CLI Tool Securely to a Specific Customer — Designing the Build, Sign, and Deliver Pipeline

Tadashi Shigeoka ·  Thu, May 28, 2026

You have built a CLI tool and you want it used only inside one customer enterprise. It can’t go on the public npm registry, but it should reach both the customer’s developers and their end-user machines with as little friction as possible. The targets are macOS and Windows, with Linux on the horizon. This is a platform-engineering problem you inevitably hit in the enterprise space.

At first glance it looks like “just package the source and hand it over,” but the reality is not that simple. Each OS aggressively blocks unsigned executables (Apple Gatekeeper, Windows Defender SmartScreen). You have to pass a tamper-evident signing protocol, and on top of that you need a distribution network whose access you can control and audit per customer. In other words, unless you design build, signing, and distribution as a single pipeline, operations will break down somewhere along the line.

This post designs that pipeline end to end: from choosing a build approach, through automated signing on macOS and Windows, to private distribution channels and supply-chain hardening. It also covers enterprise concerns specific to Japan, such as the Azure Trusted Signing geo restriction and ISMS / APPI. Since tools, available regions, and prices all change fast, treat this as a snapshot based on research from May 2026, and re-verify against the official docs as of your own check date before adopting anything.

The Big Picture — Three Layers: Build, Sign, Deliver

Before designing anything, it helps to split the problem into three layers.

  • Build: do you ship as a Node.js-runtime-dependent npm package, or as a single executable binary with the runtime baked in? This choice largely determines startup speed, distribution size, and how signing behaves.
  • Sign: on macOS, Developer ID signing plus notarization is effectively mandatory; on Windows, Authenticode signing is. Without signatures you can’t clear the OS security gates and users get unpleasant warnings.
  • Deliver: which is your primary channel: a private npm registry, a Homebrew tap, MDM, or entitlement-based delivery? The best answer depends on the customer’s environment (do they have Node.js, do they run an MDM, are they air-gapped).

The key point is that these are not independent choices: they depend on each other. For example, choosing a single binary means that on macOS you can’t staple the notarization ticket unless you wrap it in a .pkg, and if you’re shipping a .pkg then MDM or Cloudsmith become the natural channels. Let me dig into each layer in turn.

flowchart TD
    A[Git repository] --> B[CI build]
    B --> C[npm ci / test / audit]
    C --> D[bundle / compile]
    D --> E[SBOM / SHA256 / provenance]
    E --> F1[Private npm package]
    E --> F2[Windows EXE / MSI]
    E --> F3[macOS PKG]

    F2 --> G1[signtool / Azure Trusted Signing]
    F3 --> G2[codesign + notarytool + stapler]

    G1 --> H1[Intune Win32]
    G2 --> H2[Intune / Jamf macOS]
    F1 --> H3[Private npm registry]
    G1 --> I[Cloudsmith / Amazon S3 exception path]
    G2 --> I

    H1 --> J[Managed Windows devices]
    H2 --> K[Managed macOS devices]
    H3 --> L[Developers / IT admins]
    I --> M[Customer-side exception delivery]

Choosing a Build Approach — Single Binary or ncc

The first fork is whether you can assume a Node.js runtime in the customer’s environment.

If the customer has Node.js (or you can ship it) and the audience is mostly developers, the simplest path is a scoped package like @customer-scope/cli delivered via a private registry. The artifact is small (a few MB), updates go through npm update -g, and it plays well with the provenance and SBOM steps discussed later. The downside is that you take on Node.js version-mismatch risk and the build pain of native add-ons that use node-gyp.

If you need to cover end-user machines or strictly managed endpoints that require runtime independence, single-executable compilation comes into play. As of 2026, there are four main approaches.

ApproachMechanismStartup speedCross-compileSigning fitVerdict
Bun (bun build --compile)Single binary with runtime bundledFastest (JavaScriptCore, 8–15 ms class)◎ all OSes from one runner△ signing regression reported in 2026First pick for size and speed
Deno (deno compile)Single binary with runtime bundledMedium (V8)◎ any target from any host◎ clear official signing flowStrong if signing stability matters
Node.js SEAInjects a blob into the node binarySlowest (V8)✗ per-OS runner required◎ documented official resign flowFor official-first / LTS needs
@vercel/nccBundles JS and deps into one file (runtime separate)Node-equivalent○ (runtime managed separately)◎ sign the bundled runtimePragmatic choice for maintainability

Bun is implemented in Zig on top of Apple’s JavaScriptCore, and its fast cold start is the main draw. bun build --compile also cross-compiles to all OSes with no extra dependencies. The size reduction on real CLIs can be dramatic: rulesync (~140k weekly downloads) reported that moving from Deno compile to Bun shrank its darwin-arm64 binary from 565 MB to 62.8 MB, roughly a ninth of the size, though most of that gap is tree-shaking and minification, and pre-bundling narrows the difference with Deno. Note that x64 builds enable AVX2-based SIMD optimizations by default (2013 and later), so to support older CPUs you should pick a baseline build such as bun-windows-x64-baseline to avoid Illegal instruction errors.

The caveat is that Bun’s code signing has had a 2026 regression (a specific version SIGKILLs macOS arm64 binaries). If signing stability and official support are your top priorities, Node.js SEA or Deno compile have the edge. Node.js SEA requires useCodeCache and useSnapshot set to false to avoid crashes when generating cross-platform binaries, and it is currently CommonJS-centric, though build integration into core is progressing. If you’re migrating an existing pkg workflow and want a single binary, use the successor @yao-pkg/pkg.

Do not adopt the once-standard vercel/pkg for new work. The repository was archived in January 2024 (final 5.8.1), and CVE-2024-24828, a local privilege escalation via /tmp/pkg/*, remains unpatched.

In one line: Bun for startup speed and size, Node SEA or Deno for signing stability and official support, ncc plus a bundled runtime if you can assume a Node runtime and value maintainability. Whichever you pick, pin the lockfile (package-lock.json or bun.lock) and enforce npm ci / bun install --frozen-lockfile in CI to guarantee deterministic builds.

macOS — Signing, Notarization, Stapling, and the “YARA Scan Storm”

On macOS, if one of the customer’s developers tries to run an unsigned, un-notarized binary, Gatekeeper blocks it. On Apple Silicon (arm64), unsigned binaries are SIGKILLed by the kernel, so even a minimal ad-hoc signature is required.

What’s easy to miss is the “YARA scan storm” reported on macOS Sequoia (15) and later. For signed and notarized binaries, the secassessment layer caches trust locally, so the second and later launches complete instantly. But an un-notarized large binary (one report cites a Bun-based CLI reaching ~190 MB) has its cache invalidated on every launch, forcing a full YARA scan of the entire on-disk binary each time. Call the CLI rapidly from an agent loop or a CI runner, and syspolicyd CPU usage climbs to a sustained 150–200%, dragging in trustd and WindowServer until the whole machine heats up and slows down. This cannot be fundamentally resolved without embedding Apple’s notarization stamp. It’s a strong reason CLI binaries specifically need notarization.

Here is the flow for completing signing through notarization and stapling non-interactively on a GitHub Actions macOS runner.

  1. Build a temporary keychain: decode the two .p12 files stored Base64-encoded in GitHub Secrets (Developer ID Application for binary signing and Developer ID Installer for installer signing) and import them. Create an isolated keychain with security create-keychain, prevent auto-lock hangs with security set-keychain-settings -t 7200, and suppress passphrase prompts with security set-key-partition-list.
  2. Sign with Hardened Runtime: sign the compiled Mach-O binary with codesign --force --options=runtime --timestamp --entitlements entitlements.plist --sign "Developer ID Application: ...". The --options=runtime flag (Hardened Runtime) is a prerequisite for notarization. Because Bun and Node (V8) request JIT memory at runtime, you need to declare entitlements like com.apple.security.cs.allow-jit.
  3. Wrap in an installer: you cannot staple a notarization ticket to a standalone command-line binary. So build a component package with pkgbuild, assemble a distributable .pkg with productbuild, and add the installer signature with productsign --sign "Developer ID Installer: ...".
  4. Notarize and staple: submit to Apple’s notary service with xcrun notarytool submit ... --wait, which blocks the CI synchronously until verification finishes (altool is retired; notarytool is current). Once accepted, permanently embed the ticket into the package with xcrun stapler staple. Now even an internet-isolated internal environment lets Gatekeeper verify locally and install without warnings.

This assumes the Apple Developer Program (USD 99/year) and its Developer ID certificates.

Windows — Authenticode Signing and the Azure Trusted Signing Geo Restriction

The biggest obstacle on Windows is the SmartScreen warning at runtime (“Windows protected your PC”). Avoiding it effectively requires Authenticode signing.

Two things changed significantly in recent years. First, certificate tiers. In August 2024 Microsoft removed the EV code-signing OID from the Trusted Root Program, and SmartScreen now treats EV and OV equally. Neither gets an instant trust bypass; both accrue reputation through download volume. In short, there’s basically no practical reason to pick EV for ordinary software distribution anymore (kernel-mode driver signing still requires EV). Second, private-key storage. Since June 2023, a CA/Browser Forum revision mandates that the private keys of publicly trusted code-signing certificates be generated and stored inside a FIPS 140-2 Level 2-or-higher (or Common Criteria EAL4+) HSM or hardware token. Physical USB tokens can’t be accessed directly from a CI/CD cloud runner, which makes them a poor fit for automation.

This is where Azure Trusted Signing (formerly Azure Code Signing, renamed Artifact Signing in 2026) rose to prominence. On each signing operation it dynamically issues a very short-lived, disposable certificate (valid 24–72 hours) from Microsoft’s trusted root, so the user manages neither private keys nor certificate expiry. It integrates with GitHub Actions via OIDC keyless auth, and pointing signtool at the integration DLL with /dlib sends only the locally computed hash (digest) to Azure rather than the whole executable, a fast and secure signing scheme. Because it uses Microsoft’s own signing chain, SmartScreen reputation is said to accrue faster, too.

But there’s a serious constraint for organizations in Japan. As of writing, Azure Trusted Signing’s GA availability is limited to “organizations or individuals in the US and Canada, and organizations in the EU and UK,” so Japanese companies and individual developers currently can’t use it as-is. In Japan, then, the practical answer is to store the private key of an OV certificate (DigiCert, Sectigo, GlobalSign, etc.) in an Azure Key Vault HSM and sign from CI with AzureSignTool or signtool. Alternatively, you can place a Sectigo-style certificate in Google Cloud KMS HSM and sign through its CNG provider.

One more thing to factor into procurement is the shortening of certificate validity. Under CA/Browser Forum Ballot CSC-31, from March 1, 2026, the maximum validity of publicly trusted code-signing certificates drops from 39 months to 460 days (about 15 months). Multi-year prepaid discounts disappear, so you’ll need to operate on a renewal-cycle basis.

signtool sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /f cert.pfx tool.exe
signtool verify /pa /v tool.exe

Adding /fd SHA256 and an RFC 3161 timestamp (/tr and /td) is mandatory. With a timestamp, the signature remains verifiable even after the certificate expires.

Distribution Channels — Registry, Homebrew, MDM, Entitlements

How do you get the signed artifacts to the customer? The practical move is to mix channels, balancing onboarding friction against access control.

Private npm Registry

If the customer has a Node.js dev stack, this is the most natural path. Initialize the tool under a custom scope (@customer-scope/cli), and strictly control .npmignore to sanitize out internal API keys and env files so they don’t leak into the package. The customer just binds the registry endpoint and an auth token in .npmrc, integrating transparently with the normal npm toolchain.

The registry candidates differ quite a bit in character. AWS CodeArtifact leans on IAM, but its auth token’s standard lifetime is a short 12 hours, so it assumes automation (you can pick the Tokyo ap-northeast-1 region). Azure Artifacts has fine-grained feed / view / pipeline-identity permissions and fits organizations on Azure DevOps and Entra ID. GitHub Packages is tightly coupled with GitHub Actions but is constrained by PAT-classic-dependent auth. If you value air-gapped or per-customer branded delivery, Cloudsmith is also a candidate.

Enforce phishing-resistant passkey MFA (YubiKey, Windows Hello, Touch ID) on the accounts that manage publishing, and assign CI automation accounts resource-scoped Granular Access Tokens rather than legacy tokens.

Private Homebrew Tap

For customers with many macOS developers, this maximizes auto-update reach. Create a private repository (homebrew-tap) inside the customer’s organization and place signed-binary .tar.gz assets on GitHub Releases. Fetching from a private repo 404s with plain cURL, so add a custom download strategy defining GitHubPrivateRepositoryReleaseDownloadStrategy to the tap and override the protocol in the Formula. Issue each user a read-only PAT and have them declare HOMEBREW_GITHUB_API_TOKEN in their shell config; from there, brew install / brew upgrade handles both install and updates. Hook mislav/bump-homebrew-formula-action into the pipeline and the per-release Formula update (recomputing the SHA256 and committing) is automated too.

MDM (Intune / Jamf)

When the customer governs managed endpoints with an MDM, this becomes the primary channel. Note that signing and notarization remain mandatory on every channel.

With Microsoft Intune, Windows apps are registered as Win32 apps converted to .intunewin via IntuneWinAppUtil.exe, with silent install (e.g., msiexec /i app.msi /qn) and detection rules in place; assignments can be Required / Available / Uninstall. For macOS you distribute as “macOS app (PKG)” or “Line-of-business app (managed PKG),” but a managed LOB PKG requires Developer ID Installer signing. That’s a different certificate from the Developer ID Application you apply to the runtime binary, and mixing them up is a common cause of failed deployments.

With Jamf Pro, distribute a Developer-ID-Installer-signed flat .pkg via a policy or Self Service. Best practice is to include all content with no external resource dependencies, and to keep pre / postinstall scripts minimal in sh / bash / zsh.

Entitlement Delivery and the Exception Path

If you want per-customer isolated access control without account management, you can stand up a branded portal with Cloudsmith private broadcasts and gate viewing and downloads with per-customer entitlement tokens. You can revoke a token instantly on contract end or leak, and audit download activity per customer.

For an exception path when a customer’s network can’t do Intune or is isolated at the boundary, you can use Amazon S3 / CloudFront signed URLs (scoped by expiry and IP) or a shared file server. With Amazon S3, keep an audit trail via CloudTrail and server access logs. A bucket with Object Lock enabled can’t be a destination for server access logs, so separate the artifact bucket from the log bucket.

Future Linux — GPG-Signed Repositories

When you add Linux later, don’t drop unsigned binaries; build a repository that meets the OS package managers’ audit bar. For RPM, run rpmsign in CI to sign the header and payload with an internal GPG private key. Specify gpgcheck=1 and repo_gpgcheck=1 in the customer’s .repo, and DNF / YUM will fetch the public key and verify package integrity. For Debian-based systems, have debsig-verify check the _gpgorigin signature. This shuts out tampering along the supply chain.

For offline / air-gapped environments, you can use Verdaccio as a cache to carry in the needed packages, but the conclusion is that a single binary that bundles both runtime and dependencies, plus a signed .pkg / .intunewin, is the easiest to operate.

Here are the distribution channels in one table.

ChannelMain OSesCustomer-side authKey characteristics
Private scoped npmmacOS, Windows, LinuxGranular Access Token in .npmrcAssumes Node.js. Easiest to integrate vuln scanning
Private Homebrew TapmacOS, (Linux)HOMEBREW_GITHUB_API_TOKEN + PATFor macOS devs. Excellent updates via brew upgrade
MDM (Intune / Jamf)macOS, WindowsMDM assignment (Entra group / Smart Group)Easy distribution, detection, update, and audit on managed devices
Cloudsmith / Amazon S3macOS, Windows, LinuxEntitlement token / signed URLNo accounts needed. Good for per-customer audit and exception paths
APT / YUM (GPG-signed)Linux (future)GPG public-key verification of the repoHigh supply-chain integrity via metadata signing

Hardening the Supply Chain

Because the npm ecosystem layers dependencies deeply, it’s an easy target for package poisoning and dependency confusion, which makes defense at the build and distribution stages a top priority. With the 2025 Shai-Hulud worm and others, this is an area under active attack.

  • Vulnerability scanning (SCA): force-run npm audit at the start of CI and fail non-zero on Critical / High. Combine it with Snyk, Trivy, and Grype, and keep dependencies current with Dependabot / Renovate. Renovate’s cooldown feature, which lets a new version age before adoption, reduces the risk of stepping on a freshly poisoned package.
  • Deterministic builds: commit the lockfile and use frozen installs in CI with npm ci (or bun install --frozen-lockfile). npm ci fails if package-lock.json and package.json disagree. For a distributed app like a CLI, there’s also room to use npm-shrinkwrap.json to lock the published dependency tree.
  • Build provenance: use npm provenance (Sigstore + SLSA) and generate verifiable provenance (source repo URI, commit hash, build steps) with npm publish --provenance. On GitHub Actions, actions/attest-build-provenance ties SLSA provenance to the OIDC token. That said, 2026 saw malicious packages that carried valid provenance, so provenance alone isn’t enough; pushing on to build-environment isolation (SLSA Build L3) is preferable.
  • SBOM: CycloneDX (security-oriented) and SPDX (license / compliance-oriented) are the two big standards. Syft is the go-to for multi-language, dual-format output, e.g. syft dir:. -o cyclonedx-json -o spdx-json emits both at once. The CRA itself doesn’t mandate a specific format or version; supporting guidance like Germany’s BSI TR-03183 points to CycloneDX 1.6+ or SPDX 3.0.1+ as the practical baseline.

Guarantee the CLI’s own compliance in unit tests, too: implement -h / --help and -v / --version on every subcommand, return exit code 0 on success and non-zero on error, check whether output is a TTY and respect NO_COLOR / FORCE_COLOR, and trap SIGINT / SIGTERM to clean up temp files. These are must-haves for not breaking the customer’s automation scripts or CI.

Enterprise Concerns Specific to Japan

Layer onto this design the constraints particular to a Japanese customer enterprise.

First, the Azure Trusted Signing geo restriction already mentioned. The service you’d want as your first pick for Windows signing currently can’t be used as-is by Japanese organizations. For now the practical answer is an OV certificate plus Azure Key Vault, with a roadmap that considers migrating once Trusted Signing reaches GA in Japan.

Second, ISMS (ISO/IEC 27001:2022) and APPI (Japan’s personal-information protection law). ISMS audits call for automated collection of operation logs, USB-usage logs, and web-access history, and a distributed CLI can become an asset-management target. If the CLI handles personal data, mind its storage location and cross-border transfer; with CodeArtifact, choose the Tokyo region.

Third, the presence of domestic MDMs. If the customer uses CLOMO, LANSCOPE, SKYSEA, BizMobile Go!, and the like, you may need distribution steps that differ from the Intune / Jamf flow (a dedicated distribution site, or pushing .pkg / .msi). Either way, customer IT departments tend to demand silent-install support, signed and notarized artifacts, and conformance to internal proxies and change-management processes (CAB approval), and whether you can meet these determines adoption.

Conclusion

To close, here is the standard configuration I recommend (the best practice) as one concrete shape. When in doubt, start here.

  • Split distribution into two tiers
    • Developers and IT admins: a private npm registry (scoped delivery)
    • End users and managed devices: signed installers via MDM (Intune / Jamf)
    • This balances operability, auditability, and update control best
  • Choose the build by requirement
    • For developers: bundle with ncc and manage the runtime (an LTS) separately
    • Reach for a single binary only when full runtime independence is required
    • For a single binary: prefer Node SEA or Deno for signing stability, or Bun when startup and size are the top priority and you can pin a stable version
  • Bake signing into CI as a hard requirement
    • macOS: Developer ID signing + Hardened Runtime + notarization + stapling (wrap the CLI in a .pkg)
    • Windows: store an OV certificate in Azure Key Vault and sign with signtool (for Japanese organizations, since Trusted Signing is outside the GA region)
  • Draw the supply-chain baseline from day one
    • Wire npm ci with a pinned lockfile, npm audit, provenance, and SBOM into CI
    • Ratchet up to SLSA Build L3 when you have room
  • Pull all of this into a single CI pipeline
    • It produces each channel’s artifacts from the same commit

Here are the technical takeaways once more:

  • Internal distribution of a privately developed CLI is a problem of designing build, signing, and distribution as one pipeline, where each layer depends on the others.
  • For the build approach: Bun for startup speed and size, Node SEA or Deno for signing stability and official support, ncc plus a bundled runtime if you can assume a Node runtime and value maintainability. Don’t adopt vercel/pkg (unpatched CVE).
  • macOS requires Developer ID signing + Hardened Runtime + notarization + stapling. Wrap a single binary in a .pkg to staple it. Un-notarized binaries trigger the post-Sequoia “YARA scan storm” that drags the whole machine down, so notarization is unavoidable.
  • Windows assumes Authenticode signing. Azure Trusted Signing is the front-runner, but its GA region is limited, so Japanese organizations should treat OV certificate + Azure Key Vault as the practical answer for now. Certificate validity drops to a 460-day cap from March 2026.
  • For distribution, don’t funnel everything into one channel: split by use: a private registry for developers, MDM for managed devices, Cloudsmith / Amazon S3 for exceptions, and GPG-signed repositories for future Linux.
  • For the supply chain, set provenance, SBOM, npm audit, and deterministic builds as the baseline, and ratchet up to SLSA Build L3 over time.
  • As Japan-specific concerns, bake in the Trusted Signing geo restriction, ISMS / APPI, domestic MDMs, and customer IT-department constraints from the start.

One final emphasis: this space sees rapid change in tool behavior, available regions, and prices, and signing-service availability and certificate policies in particular get rewritten on short timescales. Treat this post as a May 2026 map, and always check primary sources as of your own date before adopting anything.

That’s all from the Gemba, where I designed a build, sign, and deliver pipeline for shipping a privately developed npm CLI tool securely to a specific customer.

References