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.InvariantStableView on GitHub (pinned to 71377f2769)
Solutions
- Re-run the bd command so it re-snapshots the current worktree state
- Inspect `git -C <worktree> status` and stabilize the tree (commit, stash, or clean) so it matches the planned cleanliness
- If the state change is expected and acceptable, pass the force flag to bypass the invariant check
- 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
- Keep the worktree clean (commit or stash) before planning bd operations
- Disable build/codegen or formatters that write into the worktree during the operation window
- Minimize the gap between plan creation and execution
- Re-snapshot rather than reusing a stale plan across sessions
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
- target is no longer registered at %s
- target git directory changed
- target HEAD changed from %s to %s
- target HEAD disagrees with git worktree registry (registry %
- registered target identity changed
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/cf432a9aad62ae57.
Report an issue: GitHub.