pnpm/pnpm · error · PnpmError

GIT_UNCLEAN

GIT_UNCLEAN

Error message

Unclean working tree. Commit or stash changes first.

What it means

pnpm publish runs git safety checks by default (disable with --no-git-checks); the first one requires a clean working tree, verified via isWorkingTreeClean(). Any uncommitted modification or untracked file at publish time triggers this error so that the published artifact always corresponds to a reviewable git state.

Source

Thrown at pnpm11/releasing/commands/src/publish/publish.ts:185

  opts: Omit<PublishRecursiveOpts, 'workspaceDir'> & {
    argv: {
      original: string[]
    }
    engineStrict?: boolean
    recursive?: boolean
    workspaceDir?: string
  } & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel' | 'skipManifestObfuscation' | 'versioning'>
  & Pick<ConfigContext, 'allProjects'>,
  params: string[]
): Promise<PublishResult> {
  if (opts.batch && !opts.recursive) {
    throw new PnpmError('BATCH_PUBLISH_REQUIRES_RECURSIVE', '--batch can only be used together with --recursive', {
      hint: 'Run "pnpm publish -r --batch" to publish all workspace packages in a single request.',
    })
  }
  if (opts.gitChecks !== false && await isGitRepo()) {
    if (!(await isWorkingTreeClean())) {
      throw new PnpmError('GIT_UNCLEAN', 'Unclean working tree. Commit or stash changes first.', {
        hint: GIT_CHECKS_HINT,
      })
    }
    const branches = opts.publishBranch ? [opts.publishBranch] : ['master', 'main']
    const currentBranch = await getCurrentBranch()
    if (currentBranch === null) {
      throw new PnpmError(
        'GIT_UNKNOWN_BRANCH',
        `The Git HEAD may not attached to any branch, but your "publish-branch" is set to "${branches.join('|')}".`,
        {
          hint: GIT_CHECKS_HINT,
        }
      )
    }
    if (!branches.includes(currentBranch)) {
      let isConfirmed: boolean
      try {
        isConfirmed = await confirm({

View on GitHub (pinned to 6261b7f388)

Solutions

  1. Inspect 'git status --porcelain' to see exactly what is dirty, then commit it (git add + git commit) or stash it (git stash)
  2. Add generated output to .gitignore so it stops dirtying the tree
  3. If the dirty state is expected or checks are handled elsewhere in CI, bypass with 'pnpm publish --no-git-checks'

Example fix

# before
pnpm version patch && pnpm publish   # version bump left uncommitted

# after
pnpm version patch && git add -A && git commit -m 'chore: version' && pnpm publish
Defensive patterns

Strategy: validation

Validate before calling

// Fail fast in release scripts before calling pnpm publish
import { execSync } from 'node:child_process'

const dirty = execSync('git status --porcelain', { encoding: 'utf8' }).trim()
if (dirty !== '') {
  fail(`Refusing to publish: uncommitted changes:\n${dirty}`)
}

Try / catch

try {
  await publish(opts)
} catch (err) {
  if (err instanceof PnpmError && err.code === 'GIT_UNCLEAN') {
    // surface the hint, offer to stash: execSync('git stash') then retry once
  }
}

Prevention

When it happens

Trigger: Running pnpm publish when 'git status --porcelain' is non-empty: edited files not committed, untracked build output, or a version bump written by 'pnpm version' that was never committed.

Common situations: Forgetting to commit after 'pnpm version'/'pnpm change version' bumps package.json; changeset files left untracked; generated files (dist/, coverage/) not gitignored; publishing from a machine with unrelated local edits.

Related errors


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