Lesstep
GitHub Actions · npm · Checked November 4, 2026

GitHub Actions: npm publish and "git@github.com: Permission denied (publickey)"

Nothing is wrong with SSH or with your npm token. npm read the path you gave it as a GitHub repository. An argument that does not start with ./, ../, /, ~/ or file: is tried as a GitHub shorthand before it is tried as a file (npa.js in npm-package-arg), and dist/pkg-1.0.0.tgz fits: owner dist, repository pkg-1.0.0.tgz. The .tgz at the end does not change that. npm then asks git for that repository over SSH, and the runner has no SSH key for github.com. Nothing reached the registry.

What it looks like

npm error command git --no-replace-objects ls-remote ssh://git@github.com/release-tarballs/verax-ai-inventory-0.8.0.tgz.git
npm error git@github.com: Permission denied (publickey).

The repository in the ls-remote line is your path with .git added. That is how to recognize it.

The fix: start the path with ./

- run: npm publish ./release-tarballs/verax-ai-inventory-0.8.0.tgz --provenance --access public

The same for a folder: carlesandres/house publishes ./artifacts/npm-main, not artifacts/npm-main (#351). A path with two or more slashes is read as a folder today, but npm's documentation for package specs says a folder should always start with / or ./, and calls the other reading legacy behavior that may be removed. Add ./ there too.

With a glob, a pattern that matches nothing stays as the text you wrote. dsh-plugin-toolkit checks that there is exactly one tarball before it publishes (#10):

- name: Publish
  run: |
    set -- ./dist/*.tgz
    if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
      echo "expected one tarball in dist/, found: $*"
      exit 1
    fi
    npm publish "$1" --provenance --access public

Why it shows up on a release day

The line usually fails the first time it runs:

Check it before a release

--dry-run parses the argument the same way and sends nothing. dsh-plugin-toolkit and verax both reproduced the failure this way, and verax saw npm name the spec github:release-tarballs/…:

npm publish dist/pkg-1.0.0.tgz --dry-run     # fails: read as github:dist/pkg-1.0.0.tgz
npm publish ./dist/pkg-1.0.0.tgz --dry-run   # reads the tarball, sends nothing

carlesandres/house and verax also added a test that fails when the publish line in the workflow has no ./.

After the fix, do not just re-run

A re-run uses the same GITHUB_SHA and GITHUB_REF as the run it repeats (GitHub's documentation), so a release started by a tag runs the tag's workflow again, with the old line. The projects above did one of these:

Before you publish again, check what is already on npm: npm view <name>@<version> version prints the version if it is there. npm refuses to publish over a version that exists, and a job that publishes several packages in a row may have published the ones before the failure.

Telling it apart

See failed jobs from IntelliJ IDEA

Pipeline Inbox for GitHub Actions Workflows, a plugin by Lesstep for JetBrains IDEs 2024.1 or later, lists the runs of all your repositories in an IDE tool window, with their jobs and steps, and your current branch's runs are in the status bar. Reading is free; re-running failed jobs, cancelling, running workflows and reviewing deployments are Pro (US$1.50 a month, with a 30-day trial).

GitHub and GitHub Actions are trademarks of GitHub, Inc.; npm is a trademark of npm, Inc.; IntelliJ IDEA is a trademark of JetBrains s.r.o. This page is not affiliated with, sponsored or endorsed by GitHub, Inc., npm, Inc. or JetBrains.

More: npm publish and "ENEEDAUTH" · "Artifact download failed after 5 retries" · "Artifact not found for name" · GitHub Actions plugins for IntelliJ IDEA compared · 日本語