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
- Inspect 'git status --porcelain' to see exactly what is dirty, then commit it (git add + git commit) or stash it (git stash)
- Add generated output to .gitignore so it stops dirtying the tree
- 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
- Commit immediately after pnpm version / changeset version bumps
- Gitignore generated output (dist, coverage) so builds never dirty the tree
- Keep publish as the last step of a script that ends with git push
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.