# vsce: the tool that packages and publishes VS Code extensions

> Microsoft's command line tool for building and publishing extensions, with a credential store that has moved from keytar to the native system store and a migration that asks before copying anything, plus an OIDC publishing mode that removes the Personal Access Token from CI entirely. The details of the credential migration are the part worth reading before you upgrade.

**microsoft/vscode-vsce** — VS Code Extension Manager

- Repository: https://github.com/microsoft/vscode-vsce
- Website: https://code.visualstudio.com/
- Stars: 1,053 · Forks: 259
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/microsoft-vscode-vsce

## What vsce actually does

The tool assists in packaging and publishing Visual Studio Code extensions, and the README describes itself as the Visual Studio Code Extension Manager.

It is meant to be mainly used as a command line tool, and the package exposes a `vsce` binary. It can also be used as a library, since it exposes a small API at `src/api.ts`, and the README adds a warning that when you use it as a library you should sanitize any user input used in API calls to prevent security issues.

That is a short API for a reason. The tool is designed to be run in a build script, and the library surface is there so that a larger publishing pipeline can drive it rather than shelling out.

The requirements are Node.js at least 22.x.x, which the package manifest confirms with an `engines` field of `>= 22`. The supported package managers are npm at 6 or newer and yarn at 1 but below 2.

The repository is a TypeScript project with a `src/` directory, a `build/` directory, a `vsce` bin script, an `api-extractor.json`, a `ThirdPartyNotices.txt`, a `SECURITY.md` and a `Dockerfile`. The `package.json` sets the main entry to `out/api.js` and the typings to `dist/vsce.d.ts`, and it is published as a public package named `@vscode/vsce`.

The last push was on 2026-10-15 and the recent releases are patch versions of the 4.0.1 line, v4.0.1-3 from 2026-10-03, v4.0.1-2 from the same day and v4.0.1-1 from 2026-09-29.

## The credential store moved off keytar, and the migration is deliberate

The largest part of the README is about credentials, and it is worth understanding what changed and why the change was done in the careful way it was.

On Linux, to save credentials safely, the project now uses `@napi-rs/keyring`, which uses the system Secret Service and falls back to the Linux kernel keyring. Setting the `VSCE_STORE=file` environment variable reverts to the file credential store, and using the `VSCE_PAT` environment variable avoids the system credential store entirely.

The migration from the old `keytar` store is the part with the most care in it. When a command needs a saved Personal Access Token and the publisher has none in the current native credential store, vsce checks for that publisher's old keytar credential, and if one is found in an interactive terminal it offers:

```text
A saved PAT for publisher 'your-publisher' was found in the previous credential store. Copy it to the new store? [y/N]
```

Entering `Y` copies just that publisher's PAT and continues without re-entering it. Entering `N`, or pressing Enter, continues to the normal PAT prompt without copying anything. The migration is skipped in non-interactive runs such as CI, which is the right call, because a prompt in a pipeline is a hang.

The platform behaviour differs, and the differences are documented rather than left to be discovered. On Windows, the requested Windows Credential Manager entry is read using the built-in Windows PowerShell, and no keytar installation is needed. On Linux, it requires `secret-tool`, which is `libsecret-tools` on Debian and Ubuntu, and access to the same desktop Secret Service used previously, and you may be prompted to unlock the keyring. On macOS, existing Keychain entries are already compatible and no copying is necessary.

## Consent, locking and the guarantees around the old entries

The rules around the migration are specific, and they are the kind of rules that indicate the author has thought about what can go wrong.

Existing PATs in the new store take precedence, and the migration does not run while listing publishers or logging out. Each copied PAT is read back before it is used, so a failed write is caught rather than assumed.

The clause about the old entries is the important one: old keytar entries are never modified or deleted, so older vsce versions can still use them. Declining or logging out does not permanently suppress the offer, since the old PAT can be offered again when needed, but copying always requires fresh consent. And logout removes only the new entry, it does not revoke the PAT.

That last distinction matters. A user who logs out might reasonably expect the credential to be gone from the machine, and the answer is that the copy in the new store is removed while the original stays where it was, and the Personal Access Token itself is still valid at the Marketplace.

Concurrency is handled too. Credential writes are serialized across vsce processes, and the destination is checked again after confirmation to avoid overwriting a newer PAT. The non-secret lock lives under `~/.vsce-keytar-migration`, and no PATs or consent decisions are stored there. A command waits up to 30 seconds for the lock, and a lock abandoned by a crashed process can be recovered after two minutes.

Failure is handled as a warning rather than an error. If the legacy lookup or copying fails, vsce displays a warning and continues to the normal PAT prompt, and the migration helpers time out after 30 seconds. Using `VSCE_STORE=file`, `VSCE_PAT` or `--pat` does not trigger the native-store migration, and the existing plaintext-file migration is unchanged.

So the failure mode of this whole system is: a warning, a prompt, and a token you type yourself. Nothing is lost, and nothing is silently copied.

## Trusted publishing with OIDC, and what it changes for CI

The `publish --oidc` flag publishes from GitHub Actions without storing a Personal Access Token. The setup is a trusted publishing policy for the repository and workflow on the Visual Studio Marketplace, plus permission for the workflow to request an OIDC token:

```yaml
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx @vscode/vsce publish --oidc
```

The `id-token: write` permission is the whole mechanism, and `contents: read` is deliberately all the checkout needs.

The README then documents the protocol in detail, which is unusual and useful. The publishing requests a GitHub Actions token for the `marketplace.visualstudio.com` audience and exchanges it for a Marketplace session token valid for up to 15 minutes. The exchange uses `POST /_apis/gallery/token?api-version=7.2-preview.1` with an `Authorization: FederatedToken <GitHub OIDC JWT>` header and a JSON body containing `publisherName`. The session token is then used in place of a PAT.

One detail is explicit: it does not fall back to a PAT when token acquisition or exchange fails. That is a deliberate choice, because a silent fallback would mean a CI pipeline that appears to use OIDC might actually be publishing with a long-lived credential from its secret store, and nobody would know.

The 15 minute session token is what makes this safe. There is no long-lived secret in the repository, so a compromised workflow cannot publish after the job ends, and the token expires on its own.

The dependency list supports the picture. `@azure/identity` and `azure-devops-node-api` are there for the Azure side of the Marketplace, `@vscode/vsce-sign` for extension signing, and `cockatiel` and `typed-rest-client` for the API calls.

## Configuration through flags and through package.json

The behaviour of vsce is configured with CLI flags, and the README's advice is to run `vsce --help` to list them all. The example given is:

```console
$ npx @vscode/vsce publish --baseImagesUrl https://my.custom/base/images/url
```

The same options can be set in the `package.json` so that you avoid having to retype the common options, and the example shows the shape of that block:

```jsonc
// package.json
{
  "vsce": {
    "baseImagesUrl": "https://my.custom/base/images/url",
    "dependencies": true,
    "yarn": false
  }
}
```

The `vsce` key in the package manifest is a small but meaningful feature, because the package manifest of an extension is a file the author already maintains, and putting the publishing options in it means the CI job does not need a long command line with the publisher name and the base images URL in it.

The `dependencies` and `yarn` options also carry meaning. `dependencies: true` tells the tool to include the extension's dependencies in the package, which is what you want for an extension that bundles its libraries, and `yarn: false` selects npm over yarn as the package manager for the packaging step, which matters because the tool has to run the right one to produce a correct `node_modules` tree.

The publishing side of the tool is only half of it. The other half is what goes into the package: the manifest, the files, the dependency tree and the platform-specific binaries, which is what `yauzl` and `yazl` in the dependencies are for, the zip reading and writing libraries that a `.vsix` file requires.

## Development, tests and the container image

The development instructions are short. You clone the repository, then run `npm install` and `npm run watch:build`, or `watch:test` to also build the tests. Once the watcher is running you can run out of sources with `node vsce`, and the tests run with `npm test`.

The scripts in the package manifest show how it is built. `compile` runs `tsc`, `api` runs the API extractor, and `build` chains the two. The API extractor is the interesting one, because `api-extractor.json` at the root means the public API surface is extracted and reviewed rather than being whatever the code happens to export, which is the right discipline for a tool that is also documented as a library.

The test runner is Mocha, and the README adds a note that Yarn is required to run the tests, which is a leftover from the yarn-only era given that the supported package managers line says npm 6 or newer and yarn 1 below 2.

There is a `Dockerfile` at the root for running the tool in a container. The base is node 22 on alpine, and it installs `libsecret` through apk, which is the library the Secret Service integration needs on Linux. It copies the package files, installs, copies the source, compiles, and then removes the lockfile and the tsconfig, and it declares a volume at `/workspace` with the entrypoint at `/opt/vsce/vsce`. The image is therefore designed to run against a mounted extension directory rather than to build one.

The project's licence is listed as Other, and the repository carries a `SECURITY.md`, a `ThirdPartyNotices.txt` and a `LICENSE` file.

## Where vsce is the wrong choice

The first wrong case is a team that has not decided how to publish. The credential migration, the OIDC policy, the trusted publishing configuration and the package manifest options are all things you need when you are publishing, and none of them help you while you are still deciding. The tool is aimed at the last step of a project, not at its first.

The second is a pipeline that stores the Personal Access Token in CI secrets and is happy with that. The OIDC path is better, but the migration prompt behaviour, the keyring unlocking on a headless runner and the absence of a PAT fallback all add failure modes that a stored secret does not have.

The third is a build environment that cannot reach the Marketplace token endpoint. The OIDC exchange is a specific HTTPS call to a specific path with a preview API version, and a locked-down egress policy that allows the publish call but not the token call will fail in a confusing way.

The fourth is someone who wants to build an extension for a different editor. The tool is specific to the Visual Studio Marketplace and the `.vsix` package format.

The fifth is a Node.js project below version 22. The requirement is stated twice, in the README and in the `engines` field, and a project pinned to an older LTS will need to stay on an older vsce.

What the tool does well is narrow: it turns a directory of files into a signed, versioned, published extension, and it is the reference implementation of that workflow, maintained by the same people who build the editor.

## Conclusion

vsce suits any team publishing extensions to the Visual Studio Marketplace, and the OIDC mode in particular is the right answer for a CI pipeline that has been storing a Personal Access Token in its secrets. It does not suit a team that has not yet decided how to publish, because the credential and OIDC machinery is complexity before it is needed, and it does not suit a build environment that cannot reach the Marketplace token endpoint. Before upgrading a machine that has old keytar credentials, read the migration section, because the copy is an interactive prompt that is skipped in CI and that never modifies or deletes the old entries, and note that Node 22 or newer is required.

## FAQ

### How do I publish a VS Code extension without a Personal Access Token?

Use `vsce publish --oidc` from GitHub Actions with a trusted publishing policy configured on the Marketplace and the `id-token: write` permission on the job. The tool exchanges a GitHub OIDC token for a Marketplace session token valid for up to 15 minutes, and does not fall back to a PAT if the exchange fails.

### What happened to the old keytar credentials?

The project moved to `@napi-rs/keyring`, which uses the system Secret Service and falls back to the Linux kernel keyring. In an interactive terminal vsce offers to copy a publisher's old PAT into the new store, and old keytar entries are never modified or deleted, so older versions of vsce can still use them.

### Which Node.js version does vsce need?

Node.js 22 or newer. The README states the requirement as at least 22.x.x and the package manifest sets `engines.node` to `>= 22`. The supported package managers are npm 6 or newer and yarn 1 below 2.

### Can vsce configuration live in package.json?

Yes. A `vsce` key in the package manifest can carry options such as `baseImagesUrl`, `dependencies` and `yarn`, so the common options do not have to be retyped on the command line.

### Can I use vsce as a library rather than a command line tool?

Yes, it exposes a small API in `src/api.ts`. The README warns that when using it as a library you should sanitize any user input used in API calls to prevent security issues.

## Sources

- [Issues](https://github.com/microsoft/vscode-vsce/issues)
- [microsoft/vscode-vsce on GitHub](https://github.com/microsoft/vscode-vsce)
- [Project website](https://code.visualstudio.com/)
- [README](https://github.com/microsoft/vscode-vsce/blob/main/README.md)
- [Releases](https://github.com/microsoft/vscode-vsce/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/microsoft-vscode-vsce
