Distribute an npm CLI Privately via Cloud Storage — A Self-Contained Zip Recipe with Google Drive

Tadashi Shigeoka · Wed, June 24, 2026

You built a CLI and want to hand it to a small, specific set of people. Publishing to the public npm registry is out of the question, and standing up a private registry like AWS CodeArtifact with all its auth plumbing is overkill when the audience is a handful of people. But shipping the raw repository and telling everyone to “just build it yourself” is unfriendly.

This kind of out-of-band, no-registry distribution comes up more often than you might expect: handing a preview build to an outside collaborator, delivering a PoC to a specific customer, or carrying a build by hand into an air-gapped environment. This post assembles the minimal version of that workflow as a recipe that actually runs: packing an npm CLI into a single self-contained zip and distributing it privately through a Google Drive share link. The material is the Google Drive recipe from cli-distribution-recipes, a public repository collecting one recipe per distribution channel.

To be clear about scope: this is not the article about a production-grade pipeline with signing, notarization, and supply-chain hardening. If that is what you need, see the companion post “Delivering an npm CLI Securely to Specific Customers”. What we cover here is its opposite: the lightest, lowest-effort distribution pattern there is.

When this pattern fits, and when it does not

Let me pin down the scope first. Cloud-storage zip distribution is a good match when:

  • The audience is a few to a few dozen people, where running a registry is not worth the operational overhead.
  • Recipients have Node.js and npm, so they can install a local tarball with npm install -g.
  • Auto-update and version discovery are unnecessary; you can simply tell each recipient which file to install.
  • You are not yet at the stage of guaranteeing artifact integrity and authenticity with signatures (or a checksum is good enough).

Conversely, if you need to distribute broadly to an unknown audience, push frequent versions with auto-update, or treat tamper detection as a hard requirement, this pattern will break down sooner rather than later. In those cases you should move to a private registry or signed distribution. Read this as the lightweight option that sits just before all of that.

The big picture — shipping a self-contained zip through cloud storage

The flow is simple. On the build side you produce one zip, upload it to cloud storage and configure sharing, and the recipient downloads it and installs by following the bundled instructions. That is the whole thing.

flowchart TD
    A[packages/hello-cli] --> B[npm pack]
    B --> C[codenote-net-hello-cli-0.1.0.tgz]
    D[INSTALL.md] --> E[Self-contained zip]
    C --> E
    E --> F[Upload to Google Drive]
    F --> G1[Link sharing: demo]
    F --> G2[Email-restricted sharing: production]
    G1 --> H[Download: human via browser]
    G2 --> H
    H --> I[unzip]
    I --> J[npm install -g ./*.tgz]
    J --> K[Run codenote-hello to verify]

The design crux is that the unit of distribution is neither the repository nor a bare tarball, but a self-contained zip that bundles the install instructions too. The recipient never has to read the repository: they extract the zip and follow the INSTALL.md inside it. Because the channel (cloud storage) and the contents (an installable artifact) are fully decoupled, the same zip works through any other storage with no change to the steps.

Designing the artifact — the self-contained zip

The zip holds just two files.

codenote-hello-0.1.0.zip
├── codenote-net-hello-cli-0.1.0.tgz   # output of npm pack
└── INSTALL.md                          # bundled install instructions

The tarball (.tgz) is the same archive format that npm pack produces for npm publish. Because the files field in package.json and .npmignore control what gets included, you can pack exactly the files you mean to ship without leaking internal scripts or config. The recipient installs that local tarball directly with npm install -g ./codenote-net-hello-cli-0.1.0.tgz. No registry is involved at any point.

The other file, INSTALL.md, is written so that a recipient with no knowledge of the repository can finish the job from it alone. It lists the requirements, the install command, the expected output for verification, and the uninstall step, all as copy-pasteable commands.

# Install codenote-hello
 
## Requirements
- Node.js 22 or newer
- npm
 
## Install
npm install -g ./*.tgz
 
## Verify
codenote-hello
# => Ohayou gozaimasu, Konnichiwa, Konbanwa!
 
## Uninstall
npm uninstall -g @codenote-net/hello-cli

Including the expected output (here, Ohayou gozaimasu, Konnichiwa, Konbanwa!) matters more than it looks. The recipient just compares their result against it to decide for themselves whether the install succeeded. It is the one line that guarantees reproducibility: whether a human or an agent follows the steps, the same procedure leads to the same result.

That “the job is complete from INSTALL.md alone” property has a second practical payoff: the recipient need not be a human. When the instructions are copy-pasteable commands plus an expected output to check against, you can hand the zip to an AI coding agent like Claude Code and simply say “install this by following the INSTALL.md,” and the agent will extract, install, and verify the result on its own. Writing the steps as executable commands and a pass/fail expected output, rather than natural-language prose, is exactly what makes the artifact something you can hand to either a human or an agent.

Building — npm pack, then zip

Assembling the zip by hand invites mistakes: getting the tarball version wrong, or forgetting to include INSTALL.md. It is far safer to derive the version from package.json and build the archive mechanically. The recipe uses a POSIX sh script like this.

#!/usr/bin/env sh
set -eu
 
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
PACKAGE_DIR="$REPO_ROOT/packages/hello-cli"
DIST_DIR="$SCRIPT_DIR/dist"
PACKAGE_VERSION=$(node -p "require(process.argv[1]).version" "$PACKAGE_DIR/package.json")
ZIP_NAME="codenote-hello-$PACKAGE_VERSION.zip"
 
rm -rf "$DIST_DIR"
mkdir -p "$DIST_DIR"
 
PACK_OUTPUT=$(npm pack "$PACKAGE_DIR" --pack-destination "$DIST_DIR" --json)
PACKAGE_TARBALL=$(printf '%s' "$PACK_OUTPUT" | node -e 'let input = ""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => console.log(JSON.parse(input)[0].filename));')
 
test -f "$DIST_DIR/$PACKAGE_TARBALL"
cp "$SCRIPT_DIR/INSTALL.md" "$DIST_DIR/INSTALL.md"
 
(
  cd "$DIST_DIR"
  zip -q "$ZIP_NAME" "$PACKAGE_TARBALL" INSTALL.md
)
 
printf '%s\n' "$DIST_DIR/$ZIP_NAME"

It does only three things: run npm pack --json to produce the tarball while capturing its filename machine-readably, copy INSTALL.md into the same dist/, and zip the two together. The version is read from package.json via node -p, so no human edits a number on each release. The script prints the absolute path of the generated zip to stdout, so the caller can capture it straight into a variable.

ZIP_PATH=$(recipes/google-drive/build-distribution-zip.sh)
unzip -l "$ZIP_PATH"

Use unzip -l to confirm the contents and verify that only the two files (.tgz and INSTALL.md) are present. Keep the generated dist/ in .gitignore so build artifacts never get committed to the repository.

Once the zip exists, upload it to Google Drive and decide on sharing. This is the heart of “private” distribution, and there are two modes to pick between depending on intent.

First, “anyone with the link.” This is the reproducible, demo-friendly setting. When readers try out a public-repository recipe like this one on their own machines, you use this setting and share the link. The link looks roughly like this.

https://drive.google.com/file/d/FILE_ID/view?usp=sharing

But “anyone with the link” is effectively public to whoever obtains the URL. If the link is forwarded or leaks, anyone can fetch it anonymously.

Second, restricting access to specific Google accounts by email (email-restricted sharing). This is the recommended setting for real-world use. You grant access to the recipients’ email addresses and send the link only to them. As covered below, fetching such a file requires an authenticated account, and anonymous downloads are not possible. Being able to revoke access per person, at contract end or when someone leaves, is another advantage of this mode.

Choosing “anyone with the link” for production should be reserved for cases where that genuinely is the intended access model. When in doubt, make email-restricted sharing your default.

Downloading — fetched by a human in the browser

For this distribution pattern, the most natural and safest path is a human fetching the file by hand in a browser: open the share link, confirm the file, and click download.

There are ways to automate the download from a CLI (third-party downloaders and the like), but this post does not recommend them, for two reasons. First, since you are fetching a zip with no signature or checksum, it is safer to have a person confirm with their own eyes which file they are receiving. Under this pattern’s premises (a small audience, infrequent updates), the safety of a human confirming and fetching outweighs the gains of automation. Second, it fits email-restricted sharing better. A file restricted to specific accounts can be fetched only in the context of that authenticated account, and when you are signed in to the relevant Google account in a browser, a restricted file just downloads after confirmation.

Put the other way around: “restrict access for confidentiality, yet download anonymously and automatically” is fundamentally contradictory. If you want confidentiality, access control is the premise, and the fetch is carried out by an authenticated human (or account). Keeping that straight avoids confusion.

Installing and verifying

Extract the downloaded zip and install by following the bundled INSTALL.md.

unzip codenote-hello.zip -d codenote-hello
cd codenote-hello
 
cat INSTALL.md
npm install -g ./*.tgz
codenote-hello

Finally, compare against the expected output.

Ohayou gozaimasu, Konnichiwa, Konbanwa!

If that line appears, the CLI was installed with nothing but a cloud-storage share link, never touching a registry. When you no longer need it, remove it with npm uninstall -g @codenote-net/hello-cli.

Limits and caveats

This pattern trades away several guarantees for its convenience. Before you distribute, you should state the limits to recipients explicitly.

  • No integrity or authenticity guarantee: the zip itself carries no signature or checksum, so out-of-band tampering goes undetected. As a minimum mitigation, publish the zip’s hash from the source (sha256sum codenote-hello-0.1.0.zip) over a separate channel (such as the body of an email) and have recipients verify it. To guarantee authenticity properly, move to a separate recipe that signs the artifact.
  • No version discovery or auto-update: recipients must be told which file and version to grab each time. Shipping a new version means notifying everyone to redistribute, by hand.
  • Link sharing is effectively public: “anyone with the link” loses access control the moment the URL leaks. If confidentiality matters, use email-restricted sharing.
  • Email-restricted sharing cannot be fetched anonymously: as noted above, a restricted file is downloadable only from an authenticated account. Automation and confidentiality are a trade-off here.
  • Manual upload does not scale: the upload itself is manual, so it breaks down as the audience grows. This is the minimal implementation of a distribution pattern, not a production upload pipeline.

These are design trade-offs more than flaws. When the premises break (a small audience, infrequent updates, no need yet to protect integrity with signatures), take it as the signal to move to a different channel.

Applying it to other cloud storage

Everything up to producing the self-contained zip is independent of where you distribute it. So you can drop the same artifact onto cloud storage other than Google Drive without change.

  • Dropbox / OneDrive: the model of issuing a share link and restricting access to specific accounts is the same as Google Drive, and so is the recipient’s step of opening it in a browser and downloading.
  • Amazon S3 / Cloudflare R2: presigned URLs let you constrain the expiry and source IP, giving finer access control than “anyone with the link.” They are a candidate when confidentiality matters more.
  • Shared file server / air-gap: in isolated networks, carry the zip in on physical media or an internal share and install offline exactly as the bundled INSTALL.md describes.

Whichever storage you pick, as long as the unit of distribution is the self-contained zip, the recipient’s steps (extract, install the tarball) never change. Storage is purely the choice of “how to deliver,” independent of “what to deliver.” That is the design advantage of this pattern.

Conclusion

A registry is overkill, but handing over the source feels wrong. The method that fills that gap is packing an npm CLI into a self-contained zip and distributing it privately through a cloud-storage share link. The key points:

  • Make the unit of distribution a self-contained zip containing the npm pack tarball and an INSTALL.md. Recipients can install without reading the repository.
  • Let a script derive the version from package.json and assemble the build, eliminating manual mistakes.
  • For sharing, default to “anyone with the link” for reproducible demos and email-restricted sharing for real-world use.
  • Have a human fetch the file by hand in a browser; for this lightweight pattern it is the most natural and safest path, lets the recipient confirm an unsigned zip, and fits email-restricted sharing cleanly.
  • This pattern has no integrity or authenticity guarantee, no version discovery, and no auto-update, and manual upload does not scale. When the premises break, move to signed distribution or a registry.
  • The self-contained zip is storage-agnostic, so it applies just as well to Dropbox, OneDrive, S3 presigned URLs, or an air gap.

When you need lightweight distribution, start from this minimal setup, and escalate to signing or a registry as requirements grow. Adding one more tool to your distribution toolbox lets you deliver exactly what fits the recipient and the situation.

That’s all for the minimal recipe to distribute an npm CLI privately via cloud storage like Google Drive. From the gemba.

References