gastownhall/beads · error

target cleanliness changed

Error message

target cleanliness changed

What it means

This error is thrown by bd's worktree revalidation when the target worktree's cleanliness transitioned relative to the plan: `git status` output was empty at plan time and non-empty at execution time (or vice versa). bd records this as the Cleanliness/StatusBytes invariant changing and aborts the operation because the planned snapshot no longer matches reality.

Source

Thrown at cmd/bd/worktree_cmd.go:1433

	facts.GitAdminDirectory = worktreeremove.InvariantStable
	if !sameWorktreePath(currentTarget.commonDir, plan.commonDir) {
		facts.CommonDirectory = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target common git directory changed")}
	}
	facts.CommonDirectory = worktreeremove.InvariantStable
	if currentTarget.headOID != plan.target.headOID {
		facts.Head = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target HEAD changed from %s to %s", plan.target.headOID, currentTarget.headOID)}
	}
	facts.Head = worktreeremove.InvariantStable
	if currentTarget.status != plan.target.status {
		if (currentTarget.status == "") != (plan.target.status == "") {
			facts.Cleanliness = worktreeremove.InvariantChanged
		} else {
			facts.Cleanliness = worktreeremove.InvariantStable
		}
		facts.StatusBytes = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target cleanliness changed")}
	}
	facts.Cleanliness = worktreeremove.InvariantStable
	facts.StatusBytes = worktreeremove.InvariantStable
	if currentTarget.statusFingerprint != plan.target.statusFingerprint {
		facts.DirtyFileFingerprint = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target changed files changed")}
	}
	facts.DirtyFileFingerprint = worktreeremove.InvariantStable
	if !plan.force && currentTarget.status != "" {
		facts.Cleanliness = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target is no longer clean")}
	}
	if !os.SameFile(currentTarget.pathInfo, plan.target.pathInfo) ||
		!samePinnedFileMetadata(currentTarget.pathInfo, plan.target.pathInfo) {
		facts.TargetDirectory = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target directory identity changed")}
	}
	facts.TargetDirectory = worktreeremove.InvariantStable

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-run the bd command so it re-snapshots the current worktree state
  2. Inspect `git -C <worktree> status` and stabilize the tree (commit, stash, or clean) so it matches the planned cleanliness
  3. If the state change is expected and acceptable, pass the force flag to bypass the invariant check
  4. Investigate what process modified the worktree between plan and execution and serialize access

Example fix

// before: worktree dirtied after planning
plan := snapshot(target) // clean
// ... build writes temp files into worktree ...
execute(plan) // error: target cleanliness changed
// after: keep the worktree clean or re-snapshot
run("git", "-C", wt, "status", "--porcelain") == "" || run("git", "-C", wt, "clean", "-fd")
plan := snapshot(target) // fresh
execute(plan)
Defensive patterns

Strategy: validation

Validate before calling

out, _ := exec.Command("git", "-C", wt, "status", "--porcelain").Output()
wasClean := plan.Target.Status == ""
isClean := len(out) == 0
if wasClean != isClean {
    // cleanliness flipped; re-plan before calling bd
}

Type guard

func cleanlinessMatches(plannedStatus string, wt string) bool {
    out, err := exec.Command("git", "-C", wt, "status", "--porcelain").Output()
    if err != nil { return false }
    return (len(out) == 0) == (plannedStatus == "")
}

Try / catch

err := bdRemoveWorktree(plan)
if err != nil && strings.Contains(err.Error(), "target cleanliness changed") {
    plan = rebuildPlan(plan.Target)
    err = bdRemoveWorktree(plan)
}

Prevention

When it happens

Trigger: Executing a planned worktree operation when the presence/absence of `git status` output on the target flipped between plan and execution — files were modified/created/deleted in an initially clean worktree, or a previously dirty worktree was cleaned (checkout/reset/clean) in the meantime.

Common situations: An editor or build tool wrote files into the worktree after the plan was made; the user ran `git checkout .`/`git clean` making a dirty tree clean; a concurrent agent committed or reverted changes; long gap between planning and executing in a scripted flow while the worktree was actively used.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/cf432a9aad62ae57. Report an issue: GitHub.