npm-trust
v0.11.0
Published
Bulk-configure npm OIDC Trusted Publishing for all packages in a scope
Maintainers
Readme
Built for LLM consumption. Every entry point is shaped for an agent to drive end to end:
- Filesystem auto-detection (
--auto) that removes the "what packages live here?" guesswork — works for pnpm/npm/yarn workspaces and single-package repos, picks up scope from package names automatically.--only-newfor incremental setup so the agent doesn't waste calls re-checking packages that are already trust-configured.--doctorfor a structured environment + per-package health report (JSON via--json); a Claude skill or other tooling can branch on the report's issue codes.- A typed programmatic API alongside the CLI (
discoverFromCwd,checkPackageStatuses,findUnconfiguredPackages,configureTrust, …) so an agent can choose between spawning the binary or importing the library — same primitives, same data shapes.- Deterministic output: every package status comes back as one of
configured | already | not_published | auth_failed | error, so an agent can branch on the result without parsing prose.- For an interactive guided wizard with AskUserQuestion gates, install the
gagle/solo-npmmarketplace plugin and invoke/solo-npm:trust— that skill orchestrates this CLI.
The problem
npm OIDC Trusted Publishing lets GitHub Actions publish packages without secrets or expiring tokens. But it requires per-package configuration on npmjs.com. If you maintain an npm org with 10, 50, or 100+ packages, setting up each one manually through the web UI is tedious and error-prone.
The solution
npm-trust bulk-configures OIDC Trusted Publishing for every package in your npm scope from a single command. It auto-discovers all published packages in your org, handles npm 2FA authentication once, and configures the rest automatically.
Use cases
Pick the section that matches your situation. Each one shows the command to run and what to expect.
1. First-time setup for an org
You've published a batch of packages under your npm scope and need to enable OIDC trust for all of them in one go.
npx npm-trust --scope @myorg --repo myorg/release-pipeline --workflow release.ymlThe CLI auto-discovers every published package in the scope, configures each one, and reports a summary at the end. The first package triggers a browser auth flow; on the npm site, choose "skip 2FA for the next 5 minutes" so the rest finish without further prompts.
2. Adding new packages to an existing trusted setup
You already configured OIDC for your org's packages and just published one or more new ones. Combine --scope with --only-new to filter automatically — the CLI runs npm trust list and npm view per package and configures only the ones missing trust or not yet published.
npx npm-trust --scope @myorg --repo myorg/release-pipeline --workflow release.yml --only-new--only-new works with any source (--scope, --packages, or --auto).
3. A single-package project
You maintain a standalone npm package (no monorepo, no scope-wide setup). Run from the repo root and let --auto read ./package.json:
cd ~/projects/my-package
npx npm-trust --auto --repo me/my-repo --workflow release.ymlIf you'd rather be explicit:
npx npm-trust --packages my-package --repo me/my-repo --workflow release.yml4. A monorepo (pnpm / npm / yarn workspaces, with or without NX)
You maintain a monorepo with multiple publishable packages — for example packages/foo, packages/bar, apps/something. Run from the repo root with --auto:
cd ~/projects/my-monorepo
npx npm-trust --auto --repo myorg/repo --workflow release.ymlDetection priority: pnpm-workspace.yaml → package.json#workspaces → single ./package.json. Packages marked private: true are skipped.
If every published package shares the same scope, --scope @myorg is also a one-liner. For ad-hoc lists, --packages still works.
5. Auditing — checking what's already trusted
To inspect current trust status without making changes:
npx npm-trust --scope @myorg --listTo preview what configure would do:
npx npm-trust --scope @myorg --repo myorg/repo --workflow release.yml --dry-runWhat happens during execution
- The CLI discovers all packages in your npm scope (or uses the list you provide).
- For each package, it runs
npm trust githubto configure OIDC trust. - If npm requires 2FA, the CLI pauses and opens a browser-based authentication prompt. You authenticate once — npm caches the session for ~5 minutes, long enough to configure the remaining packages.
- Packages already configured are silently skipped. Unpublished packages are reported so you know to publish them first.
- At the end, a summary shows how many were configured, already set, or failed.
Requirements
- Node.js >= 24.0.0
- npm >= 11.5.1 (for
npm trustsupport) - 2FA enabled on your npm account
- Write access to the packages you're configuring
Options
| Flag | Description |
| --------------------- | ------------------------------------------------------------------------------------------ |
| --scope <scope> | npm org scope (e.g. @myorg) — auto-discovers all published packages |
| --packages <pkg...> | explicit package names (alternative to --scope) |
| --auto | detect packages from the current directory (workspaces or single package.json) |
| --repo <owner/repo> | GitHub repository |
| --workflow <file> | GitHub Actions workflow filename (e.g. release.yml) |
| --list | list current trust status instead of configuring |
| --only-new | filter the resolved package list to those without OIDC trust yet, or not yet published |
| --doctor | print a structured health report (environment, repo, packages); exit 1 on fail issues |
| --validate-only | fast read-only pre-flight (workflow + repo + auth, no per-package npm calls) |
| --verify-provenance | bulk-query provenance attestations for the discovered/named packages |
| --emit-workflow | print the canonical OIDC release.yml template to stdout |
| --with-prepare-dist | modifier for --emit-workflow; emits the variant that wires in gagle/prepare-dist@v1 |
| --capabilities | emit a CapabilitiesReport JSON describing the CLI surface (for tool-discovery) |
| --json | emit machine-readable JSON (works with --doctor, --list, --validate-only, --verify-provenance, --capabilities, and configure) |
| --dry-run | show what would be done without making changes |
| --help | show help message |
Downstream consumers should pin to the public CLI contract documented in
docs/cli-api.md— exit codes, JSON schemas, and migration notes.
Example output
Discovering packages in scope @myorg...
Found 12 packages
Configuring OIDC trusted publishing for 12 packages in @myorg
Repo: owner/repo | Workflow: release.yml
@myorg/core ✓ configured
@myorg/cli ✓ configured
@myorg/utils ✓ already configured
@myorg/new-pkg ✗ not published yet
Done: 2 configured, 9 already set, 1 failed
Failed packages (publish first, then re-run):
- @myorg/new-pkgProgrammatic usage
npm-trust is published as a dual CLI + library. The same package exposes a typed public API for use inside other tools, scripts, or CIs.
import {
checkPackageStatuses,
configureTrust,
discoverFromCwd,
discoverPackages,
findUnconfiguredPackages,
listTrust,
runCli,
} from "npm-trust";discoverPackages(scope)
Discovers all published packages in an npm scope by paginating the public registry search API.
| Parameter | Type | Description |
| --------- | -------- | ---------------------------------------------------------------------- |
| scope | string | The npm scope (with or without leading @, e.g. @myorg or myorg). |
Returns: Promise<Array<string>> — sorted package names in the scope.
Throws if the registry response is malformed, the registry URL is invalid, or the request times out (15 s).
const packages = await discoverPackages("@myorg");discoverFromCwd(cwd)
Detects packages from a directory's filesystem layout: pnpm-workspace.yaml → package.json#workspaces → single ./package.json. Packages marked private: true are skipped.
| Parameter | Type | Description |
| --------- | -------- | -------------------------------------------------------- |
| cwd | string | Absolute path to the directory to inspect (often process.cwd()). |
Returns: Promise<DiscoveredWorkspace | null>
interface DiscoveredWorkspace {
readonly source: "pnpm-workspace" | "npm-workspace" | "single-package";
readonly packages: ReadonlyArray<string>;
}const detected = await discoverFromCwd(process.cwd());
if (detected !== null) {
console.log(`Found ${detected.packages.length} packages in ${detected.source}`);
}checkPackageStatuses(packages) and findUnconfiguredPackages(packages)
Both call three npm commands per package — npm trust list <pkg> (existing trust record), npm view <pkg> name (publication), and npm view <pkg> dist.attestations.url (SLSA provenance attestation).
checkPackageStatuses returns the full status of every package — useful for orchestration code (e.g. a wizard) that needs to differentiate "unpublished" from "missing trust" from "trusted via the npm web UI":
interface PackageStatus {
readonly pkg: string;
readonly trustConfigured: boolean;
readonly published: boolean;
readonly hasProvenance: boolean;
}hasProvenance cross-checks the registry for a SLSA provenance attestation on the latest version. It catches the common case where Trusted Publishing was set up via npm's web UI rather than the CLI — npm trust list returns empty in that scenario, but the package's publishes are still going through OIDC.
findUnconfiguredPackages is a convenience filter over the same data. A package is kept (i.e. flagged as needing work) only when it lacks both an explicit trust record and a provenance attestation, or isn't yet published. Concretely: kept if !((trustConfigured || hasProvenance) && published).
const statuses = checkPackageStatuses(["@myorg/foo", "@myorg/new"]);
const unconfigured = findUnconfiguredPackages(["@myorg/foo", "@myorg/new"]);configureTrust(options)
Runs npm trust github <pkg> --repo <r> --file <w> --yes for every package and aggregates the results.
| Option | Type | Default | Description |
| -------------- | -------------------------- | ------------------ | ------------------------------------------------------------------------ |
| packages | ReadonlyArray<string> | (required) | Package names to configure. |
| repo | string | (required) | GitHub owner/repo. |
| workflow | string | (required) | GitHub Actions workflow file (*.yml / *.yaml). |
| dryRun | boolean | false | Print what would happen without invoking npm. |
| logger | Logger | console | { log, error } — supply a capturing logger to suppress stdout. |
2FA:
npm trustuses web-based 2FA only — there is no OTP/TOTP flag to pass programmatically. The first call opens a browser auth flow; on the npm site, enable the "skip 2FA for the next 5 minutes" option to let bulk calls proceed without re-authenticating.configureTrustfalls back to an interactive prompt automatically when run from a TTY; in non-TTY contexts (CI without an OIDC issuer) it returnsauth_failedimmediately.
Returns: TrustSummary
interface TrustSummary {
readonly configured: number;
readonly already: number;
readonly failed: number;
readonly failedPackages: ReadonlyArray<string>;
}const summary = configureTrust({
packages: ["@myorg/foo", "@myorg/bar"],
repo: "owner/repo",
workflow: "release.yml",
});
if (summary.failed > 0) {
console.error("Failed:", summary.failedPackages);
process.exit(1);
}listTrust(options)
Runs npm trust list <pkg> for each package and prints the current trust configuration.
| Option | Type | Default | Description |
| ---------- | ----------------------- | ---------- | ------------------------------------------ |
| packages | ReadonlyArray<string> | (required) | Package names to query. |
| logger | Logger | console | { log, error } — destination for output. |
Returns: void.
listTrust({ packages: await discoverPackages("@myorg") });runCli(argv, logger?)
The same entry point used by the bin script. Useful for embedding the full CLI behaviour inside larger tools without spawning a child process. Returns the exit code instead of calling process.exit.
| Parameter | Type | Default | Description |
| --------- | ---------------------------- | --------- | ----------------------------------------------------------------- |
| argv | ReadonlyArray<string> | (required) | The argument list, equivalent to process.argv.slice(2). |
| logger | Logger | console | { log, error } — supply a capturing logger to suppress output. |
Returns: Promise<number> — process exit code.
const code = await runCli(["--scope", "@myorg", "--list"]);
process.exit(code);Health check (--doctor)
--doctor produces a single structured report covering everything an agent or
human needs before running a trust setup: Node + npm versions, npm auth, the
detected workspace, the GitHub remote, candidate workflow files, and per-
package trust + provenance state. Plus a list of issues with stable codes.
npx npm-trust --doctor # human-readable, color when stdout is a TTY
npx npm-trust --doctor --json # machine-parseable JSON for agents and CIExit code is 0 when no fail-severity issues exist, 1 otherwise. Warnings
don't fail the gate. Stable issue codes (for agents to branch on):
| Code | When |
|---|---|
| NODE_TOO_OLD (fail) | Node < 24 |
| NPM_TOO_OLD / NPM_UNREACHABLE | npm < 11.5.1 or not on PATH |
| AUTH_NOT_LOGGED_IN | npm whoami failed |
| AUTH_REGISTRY_UNUSUAL | registry isn't https://registry.npmjs.org |
| WORKSPACE_NOT_DETECTED / WORKSPACE_EMPTY | no signals or all packages private |
| REPO_NO_REMOTE / REPO_REMOTE_NOT_GITHUB | no origin or non-GitHub origin |
| WORKFLOWS_NONE / WORKFLOWS_AMBIGUOUS / WORKFLOW_NOT_FOUND | publish workflow problems |
| PACKAGE_NOT_PUBLISHED | package not yet on the registry |
| PACKAGE_TRUST_DISCREPANCY | npm trust list empty but registry has SLSA provenance for the package |
| REGISTRY_UNREACHABLE | sentinel registry call failed |
| DOCTOR_FLAG_IGNORED | --auto / --scope / --packages was supplied alongside --doctor and ignored |
The JSON shape is DoctorReport from the public types. schemaVersion is
versioned; consumers should switch on it before reading other fields.
Use from a Claude Code agent
The CLI is callable directly from any agent that can spawn shell
commands — npx npm-trust --auto --repo <owner/repo> --workflow
release.yml, etc. For an interactive guided wizard with AskUserQuestion
gates, install the gagle/solo-npm
marketplace plugin:
/plugin marketplace add gagle/solo-npm
/plugin install solo-npm@gllamas-skillsThen invoke /solo-npm:trust for the OIDC trust wizard (which calls
this CLI under the hood). Or /solo-npm:init for the full bootstrap
umbrella that scaffolds release.yml + publishConfig + .nvmrc + consumer
wrappers and then chains into trust.
Release workflow for solo AI devs
npm-trust is the OIDC-trust primitive in an opinionated release
workflow built for solo developers (or small groups of LLM agents)
shipping AI-generated code. The full lifecycle — bootstrap, release,
verify, status, audit, deps — lives in the
gagle/solo-npm marketplace plugin
as seven Claude Code skills. solo-npm's /solo-npm:trust skill is what
calls this CLI.
/plugin marketplace add gagle/solo-npm
/plugin install solo-npm@gllamas-skillsFirst publish (chicken-and-egg)
OIDC trusted publishing requires the package to already exist on the npm registry before trust can be configured (npm needs the package record to attach trust to). That creates a chicken-and-egg: you can't publish via OIDC + provenance until trust is set up, but trust can't be set up until the package exists.
The bootstrap path:
- Develop the MVP locally; bump
package.json#versionto the initial release (e.g.,0.1.0). - Classic publish, locally, ONE time:
The package now exists on npm.npm login # web 2FA, browser flow npm publish --access public # without --provenance - Configure OIDC trust via this CLI:
Or invokepnpm exec npm-trust --auto --repo <owner/repo> --workflow release.yml/solo-npm:trustin Claude Code for the full guided flow (auth gate, dry-run, configure, verify). - Set
package.json#publishConfig:"publishConfig": { "access": "public", "provenance": true } - From here, every subsequent release uses the tag-triggered CI workflow — no more classic publishes. CI handles the publish via OIDC + provenance.
Step 2 is the only place a human is at the keyboard for the bootstrap
beyond the single AskUserQuestion approval in /solo-npm:release.
Everything else is agent-driven once trust is configured.
Environment variables
| Variable | Default | Description |
| ------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| NPM_TRUST_NPM | <dirname(process.execPath)>/npm | Override the path to the npm binary. Used in tests; rarely needed in production. |
| NPM_TRUST_REGISTRY | https://registry.npmjs.org | Override the registry used for package discovery. Must be https://..., or http://localhost / http://127.0.0.1 for local mirrors and tests. |
