pnpm/pnpm · error · PnpmError

ERR_PNPM_GIT_CHECKOUT_FAILED

ERR_PNPM_GIT_CHECKOUT_FAILED

Error message

received commit ${receivedCommit.trim()} does not match expected value ${resolution.commit}

What it means

After cloning (or shallow-fetching) and checking out resolution.commit, the git fetcher reads back HEAD via git rev-parse and requires an exact string match with the pinned commit. A mismatch means the working copy did not land on the pinned SHA — typically a shallow fetch that could not honor the exact commit, or upstream history rewritten between resolution and fetch.

Source

Thrown at pnpm11/fetching/git-fetcher/src/index.ts:49

      throw new PnpmError('INVALID_GIT_COMMIT', `Invalid git commit hash "${resolution.commit}" for repository "${resolution.repo}". Expected a 40-character hexadecimal SHA.`)
    }
    const tempLocation = await cafs.tempDir()
    try {
      if (allowedHosts.size > 0 && shouldUseShallow(resolution.repo, allowedHosts)) {
        await execGit(['init'], { cwd: tempLocation })
        await execGit(['remote', 'add', 'origin', resolution.repo], { cwd: tempLocation })
        await execGit(['fetch', '--depth', '1', 'origin', resolution.commit], { cwd: tempLocation })
      } else {
        await execGit(['clone', resolution.repo, tempLocation])
      }
    } catch (err: unknown) {
      assert(util.types.isNativeError(err))
      throw gitFetchError(err, resolution.repo, opts.pkg?.name)
    }
    await execGit(['checkout', resolution.commit], { cwd: tempLocation })
    const receivedCommit = await execGit(['rev-parse', 'HEAD'], { cwd: tempLocation })
    if (receivedCommit.trim() !== resolution.commit) {
      throw new PnpmError('GIT_CHECKOUT_FAILED', `received commit ${receivedCommit.trim()} does not match expected value ${resolution.commit}`)
    }
    let pkgDir: string
    try {
      const prepareResult = await preparePackage({
        allowBuild: opts.allowBuild,
        ignoreScripts: createOpts.ignoreScripts,
        pkgResolutionId: createGitHostedPkgId(resolution),
        unsafePerm: createOpts.unsafePerm,
        userAgent: createOpts.userAgent,
      }, tempLocation, resolution.path ?? '')
      pkgDir = prepareResult.pkgDir
      if (ignoreScripts && prepareResult.shouldBeBuilt) {
        globalWarn(`The git-hosted package fetched from "${resolution.repo}" has to be built but the build scripts were ignored.`)
      }
    } catch (err: unknown) {
      assert(util.types.isNativeError(err))
      err.message = `Failed to prepare git-hosted package fetched from "${resolution.repo}": ${err.message}`
      throw err

View on GitHub (pinned to 6261b7f388)

Solutions

  1. Re-run the install — if the mismatch was a transient race, a fresh resolve+fetch succeeds
  2. Update the pinned commit in the lockfile to the current upstream SHA (regenerate the lockfile)
  3. Remove the host from gitShallowHosts (config) so a full clone is used instead of a shallow fetch

Example fix

# before: pinned commit no longer reachable after upstream force-push
$ pnpm install  # ERR_PNPM_GIT_CHECKOUT_FAILED: received commit abc... does not match expected def...

# after: re-resolve to the rewritten history
$ rm pnpm-lock.yaml && pnpm install
Defensive patterns

Strategy: try-catch

Validate before calling

import { execa } from 'execa'

// Confirm the pinned commit is still the upstream tip-ish ref before fetching
async function assertCommitReachable (repo: string, commit: string): Promise<void> {
  const { stdout } = await execa('git', ['ls-remote', repo])
  const reachable = stdout.split('\n').some(line => line.startsWith(commit))
  if (!reachable) {
    throw new Error(`${commit} no longer advertised by ${repo}; history was probably rewritten — re-resolve`)
  }
}

Try / catch

try {
  await fetchers.git(cafs, resolution, opts)
} catch (err) {
  if (err instanceof PnpmError && err.code === 'ERR_PNPM_GIT_CHECKOUT_FAILED') {
    // Either a force-push race (re-resolve and retry once) or shallow-fetch weirdness
    const fresh = await reResolveGitDependency(resolution.repo)
    if (fresh.commit === resolution.commit) throw err // deterministic — do not loop
    return fetchers.git(cafs, { ...resolution, commit: fresh.commit }, opts)
  }
  throw err
}

Prevention

When it happens

Trigger: The repo host is in gitShallowHosts and git fetch --depth 1 origin <commit> yields a different HEAD; the upstream force-pushed between resolve-time and fetch-time; a corrupted local git cache served the wrong commit.

Common situations: Racing an upstream force-push during CI; exotic git hosting with partial shallow-fetch support; rare flaky mirrors.

Related errors


AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17). Data as JSON: /api/errors/a004ea9dfcc7d30e. Report an issue: GitHub.