gastownhall/beads · error

target is no longer clean

Error message

target is no longer clean

What it means

This error aborts a planned bd worktree operation when the target worktree was clean when the plan was built but now has `git status` output, and the plan was not created with force. bd refuses to remove or mutate a worktree that has accumulated uncommitted changes since planning, to avoid destroying work.

Source

Thrown at cmd/bd/worktree_cmd.go:1444

	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
	if !os.SameFile(currentTarget.gitDirInfo, plan.target.gitDirInfo) ||
		!samePinnedFileMetadata(currentTarget.gitDirInfo, plan.target.gitDirInfo) {
		facts.GitAdminDirectory = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target git directory identity changed")}
	}
	facts.GitAdminDirectory = worktreeremove.InvariantStable
	if !os.SameFile(currentTarget.gitMarkerInfo, plan.target.gitMarkerInfo) ||
		!samePinnedFileMetadata(currentTarget.gitMarkerInfo, plan.target.gitMarkerInfo) {
		facts.GitMarker = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target git marker identity changed")}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect `git -C <worktree> status`, then commit or stash the new changes before retrying the bd operation
  2. If the changes are disposable artifacts, run `git -C <worktree> clean -fd` (and `git checkout -- .` for tracked edits) to restore cleanliness, then re-run
  3. Re-run the bd command so the plan reflects the current (dirty) state; a fresh plan for a dirty tree will either proceed appropriately or prompt for force
  4. If the dirty state is intentional and you accept the risk, retry with the force flag

Example fix

// before: removing a worktree that became dirty
executeRemove(plan) // error: target is no longer clean
// after: stabilize first
run("git", "-C", wt, "stash", "-u") // or commit
executeRemove(rebuildPlan(wt)) // succeeds
Defensive patterns

Strategy: validation

Validate before calling

if !plan.Force {
    out, _ := exec.Command("git", "-C", wt, "status", "--porcelain").Output()
    if len(out) > 0 {
        // commit, stash, or clean before calling bd; or require force
    }
}

Type guard

func cleanOrForced(force bool, wt string) bool {
    if force { return true }
    out, err := exec.Command("git", "-C", wt, "status", "--porcelain").Output()
    return err == nil && len(out) == 0
}

Try / catch

err := bdRemoveWorktree(plan)
if err != nil && strings.Contains(err.Error(), "target is no longer clean") {
    // surface to the user: uncommitted changes detected; do NOT auto-force
    return fmt.Errorf("worktree %s has new uncommitted changes; commit/stash or retry with --force", wt)
}

Prevention

When it happens

Trigger: Calling the planned worktree remove/operation with plan.force == false when the target's `git status --porcelain` is non-empty at execution time but was empty at plan time — i.e. any new modification, staged change, or untracked file appeared after planning.

Common situations: The developer (or an IDE) started editing files in the worktree after running the bd command; a build or test run wrote untracked artifacts; a background sync pulled in changes; scripted flows where planning happened minutes before execution while the tree was actively developed on.

Related errors


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