pnpm/pnpm · error · PnpmError

INVALID_GIT_COMMIT

INVALID_GIT_COMMIT

Error message

Invalid git commit hash "${resolution.commit}" for repository "${resolution.repo}". Expected a 40-character hexadecimal SHA.

What it means

Before touching the network, the git fetcher validates resolution.commit: it must be a full 40-character hexadecimal SHA. Anything else — a branch name like 'main', a tag, or an abbreviated SHA — fails isValidCommitHash. This is both a correctness requirement (checkout needs an immutable ref) and a safety check on the value passed to git.

Source

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

import { addFilesFromDir } from '@pnpm/worker'
import { rimraf } from '@zkochan/rimraf'
import { safeExeca as execa } from 'execa'

export interface CreateGitFetcherOptions {
  gitShallowHosts?: string[]
  storeIndex: StoreIndex
  unsafePerm?: boolean
  userAgent?: string
  ignoreScripts?: boolean
}

export function createGitFetcher (createOpts: CreateGitFetcherOptions): { git: GitFetcher } {
  const allowedHosts = new Set(createOpts?.gitShallowHosts ?? [])
  const ignoreScripts = createOpts.ignoreScripts ?? false

  const gitFetcher: GitFetcher = async (cafs, resolution, opts) => {
    if (!isValidCommitHash(resolution.commit)) {
      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}`)

View on GitHub (pinned to 5b11d3a15b)

Solutions

  1. Regenerate the lockfile so the git resolver records the full SHA: rm pnpm-lock.yaml && pnpm install
  2. Reference git deps by tag or branch in package.json and let pnpm resolve and lock the full SHA
  3. Never hand-edit resolution.commit fields

Example fix

# before - hand-edited lockfile
resolution:
  commit: main

# after - locked full SHA (regenerate the lockfile)
resolution:
  commit: 9f8e7d6c5b4a3928172635445362718990aabbcc
Defensive patterns

Strategy: validation

Validate before calling

const COMMIT_SHA = /^[0-9a-f]{40}$/i

function isFullCommitSha (commit: string | undefined): boolean {
  return commit != null && COMMIT_SHA.test(commit)
}

if (!isFullCommitSha(resolution.commit)) {
  throw new Error(`resolution.commit must be a 40-char hex SHA, got: ${String(resolution.commit)}`)
}

Try / catch

catch code === 'INVALID_GIT_COMMIT'; respond by regenerating the lockfile rather than patching the field by hand

Prevention

When it happens

Trigger: resolution.commit is not a 40-hex SHA: hand-edited lockfiles storing branch or tag names or short SHAs; custom tooling or resolvers emitting non-SHA commits; lockfiles produced by an incompatible pnpm version.

Common situations: Manually 'simplifying' lockfile git entries; scripts that rewrite lockfiles; mixed pnpm versions across a team writing different resolution shapes.

Related errors


AI-assisted analysis of pnpm/pnpm@5b11d3a15b (2026-08-16). Data as JSON: /api/errors/78999d2b5f1072ab. Report an issue: GitHub.