CI & automation
Install and publish from CI, Vercel and coding-agent sandboxes with a vlt.io service token.
Store your token as a secret named VLT_TOKEN and expose it to the
job as an env var. The config files in these guides reference it
(${VLT_TOKEN}, or $VLT_TOKEN in bunfig.toml), so nothing else
changes between your laptop and CI — except pnpm, see
below.
Who reads what
Only the vlt CLI reads VLT_TOKEN itself (with VLT_REGISTRY, or
per-registry VLT_TOKEN_*; see
Authentication).
Other tools see the secret only through an auth line that references
it (pnpm: ~/.npmrc or a pnpm_config_… env var), so setting the
secret alone sends no credentials.
| Tool | Auth from | Token reference in committed project file |
|---|---|---|
| vlt | keychain or env vars | n/a, .npmrc not read |
| npm, npx | .npmrc | expanded |
| pnpm | ~/.npmrc, or pnpm_config_… env var | ignored (11.5.3+) |
| yarn | .yarnrc.yml (Classic: .npmrc) | expanded |
| bun | bunfig.toml ($VLT_TOKEN) or .npmrc | expanded |
| deno | .npmrc | expanded |
| Vercel builds | .npmrc written from NPM_RC | see Vercel |
GitHub Actions
name: ci
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
env:
VLT_TOKEN: ${{ secrets.VLT_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm testCommit the .npmrc / bunfig.toml / .yarnrc.yml from the relevant
guide. Because they reference ${VLT_TOKEN} rather than a literal
token, they are safe in version control and need no rewriting step in
CI (pnpm: see below).
Per package manager
# npm — commit .npmrc with ${VLT_TOKEN}
npm ci
# pnpm — pin to 11.26.0, and pass the token as a config env var (see below)
env "pnpm_config_//registry.vlt.io/acme/npm/:_authToken=$VLT_TOKEN" \
pnpm install --frozen-lockfile
# yarn — commit .yarnrc.yml with ${VLT_TOKEN}
yarn install --immutable
# bun — commit bunfig.toml with $VLT_TOKEN
bun install --frozen-lockfile
# deno — commit .npmrc with ${VLT_TOKEN}
deno ci
# vlt — one VLT_TOKEN_<registry> per registry
VLT_TOKEN_https_registry_vlt_io_acme_npm=$VLT_TOKEN \
VLT_TOKEN_https_registry_vlt_io_acme_main=$VLT_TOKEN \
vlt install --frozen-lockfileGenerating .npmrc at runtime instead
If you would rather not commit the file, write it in a step:
- run: |
cat > .npmrc <<'EOF'
registry=https://registry.vlt.io/acme/npm/
@acme:registry=https://registry.vlt.io/acme/main/
//registry.vlt.io/acme/npm/:_authToken=${VLT_TOKEN}
//registry.vlt.io/acme/main/:_authToken=${VLT_TOKEN}
EOFKeep the quotes on <<'EOF' so the shell leaves ${VLT_TOKEN} intact
for the package manager to expand — never write the literal token into
a project .npmrc. pnpm ignores ${VLT_TOKEN} here too; see below.
pnpm needs a different approach
pnpm ignores ${VLT_TOKEN} in a project .npmrc on purpose, so the
committed-file pattern above does not carry credentials for it. Commit
only the routing, and supply the token as a config environment
variable:
- run:
env
"pnpm_config_//registry.vlt.io/acme/npm/:_authToken=$VLT_TOKEN"
pnpm install --frozen-lockfileUse env rather than export: the variable name contains /, :
and ., which the shell will not accept in an assignment. This needs
pnpm 11.6.0 or newer; see
the pnpm guide for the full version
matrix.
Or write the auth lines to the user-level ~/.npmrc, where pnpm still
expands ${VLT_TOKEN}:
- run: |
cat >> ~/.npmrc <<'EOF'
//registry.vlt.io/acme/npm/:_authToken=${VLT_TOKEN}
//registry.vlt.io/acme/main/:_authToken=${VLT_TOKEN}
EOF
- run: pnpm install --frozen-lockfilePNPM_CONFIG_NPMRC_AUTH_FILE=<path> (env form of --npmrc-auth-file)
does the same with an auth file outside the repo (verified on pnpm
11.26 and 12).
Pin pnpm by exact version in packageManager, not by range. Corepack
resolves a range against its local cache first, so pnpm@11 can
produce different versions on different runners.
Two notes on the other tools
- deno —
deno ci(Deno 2.8+) is the equivalent ofnpm ci: it installs strictly fromdeno.lockand fails rather than updating it. - deno — Deno's default 24-hour dependency-age gate will block a
package you published minutes earlier. If CI installs your own fresh
releases, configure
minimumDependencyAgeas shown in the deno guide.
Vercel
Vercel doesn't read VLT_TOKEN. Builds install with an .npmrc
Vercel writes from the NPM_RC env var. Set NPM_RC to your routing
and auth lines with a package:read service token, and keep a
registry.npmjs.org line — Vercel installs its runtimes from the
public registry. For multi-line values, set it with the Vercel CLI to
keep the line breaks. Use NPM_RC, not NPM_TOKEN (meant for private
packages on npm's own registry; NPM_RC wins if both are set). See
Vercel's
private dependencies guide.
Coding-agent sandboxes
Each cloud agent or dev environment (Cursor, Claude Code, Devin, Codespaces, …) needs the token set up separately — treat each like a CI runner:
- Give it its own
package:readservice token (owners and admins create them), so you can revoke one without breaking the others. - Store the token in that environment's secrets (these guides call it
VLT_TOKEN). - Commit the routing: default
registry→ thenpmmirror,@acmescope →main(.npmrc/.yarnrc.yml/bunfig.toml/vlt.json). - Provide auth for both registry URLs: committed
${VLT_TOKEN}lines (npm, yarn, deno;$VLT_TOKENinbunfig.toml), a setup step writing~/.npmrc(pnpm, above), or, for vlt, the env vars in Authentication.
Publishing from CI
Use a service token. When you create the token at
vlt.io → Tokens, choose This is a service
token rather than This token is for myself, and give it
package:read and package:write.
That choice is what makes unattended publishing work. A personal token answers every publish with a one-time-password challenge and fails in CI with:
npm error code EOTP
npm error This operation requires a one-time password.A service token has no such challenge — verified with npm, pnpm,
yarn, bun and vlt, all publishing to main with no prompt.
- run: npm publish
env:
VLT_TOKEN: ${{ secrets.VLT_TOKEN_CI }}Nothing else about the config changes; a service token is a drop-in replacement in every snippet in these guides. Store it as a separate secret from your personal token so it is obvious which one CI uses.
Three per-tool notes:
- yarn needs
yarn installbeforeyarn npm publish, or it aborts withThis package doesn't seem to be present in your lockfile. - pnpm needs
--no-git-checksin CI, since the checkout is often not on the publish branch. - deno publishes in two steps —
deno packthennpm publish <tarball>. Setpublish.includeindeno.jsonfirst, ordeno packmay bundle your.npmrcinto the tarball. See the deno guide.
If you are stuck with a personal token, vlt publish can take a
pre-obtained code:
VLT_OTP=<code> vlt publishRead-only installs of public packages
A job that installs only public packages from main needs no secret
at all — just the registry URL:
@acme:registry=https://registry.vlt.io/acme/main/The npm mirror always requires a token, so this only helps if every
one of your dependencies is a public package from main.
Checklist
VLT_TOKENis set as a masked secret, not a plaintext variable.- The committed config uses
${VLT_TOKEN}(or$VLT_TOKENforbunfig.toml; pnpm: routing only) — never a literal token. - The secret is really set in the job: npm, bun and deno send an unset token variable literally, which looks like a bad token.
- After switching registries: npm and pnpm lockfiles kept;
vlt-lock.json,yarn.lock,bun.lockanddeno.lockregenerated (see each guide). - Installs use the frozen-lockfile flag for the package manager.