gastownhall/beads · error

registered target identity changed

Error message

registered target identity changed

What it means

During revalidation, bd recomputes a stable identity for the registered target worktree (path plus lock/lockReason/prunable/pruneReason). If it differs from the identity captured when the plan was built, the registry entry was mutated underneath bd — for example the worktree was locked, unlocked, or became prunable — and bd aborts rather than removing a worktree under changed conditions.

Source

Thrown at cmd/bd/worktree_cmd.go:1401

func (plan *worktreeRemovalPlan) observeRevalidation(ctx context.Context) worktreeRevalidationObservation {
	facts := worktreeremove.RevalidationFacts{}
	worktrees, err := listRegisteredWorktrees(ctx, plan.git, plan.executionRoot)
	if err != nil {
		return worktreeRevalidationObservation{facts: facts, err: err}
	}
	currentEntry, found := findRegisteredWorktreeByPath(worktrees, plan.target.path)
	if !found {
		facts.Registration = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target is no longer registered at %s", plan.target.path)}
	}
	if worktreeRegistryIdentity(currentEntry) != plan.target.registryID {
		facts.Registration = worktreeremove.InvariantChanged
		if currentEntry.locked != plan.target.locked || currentEntry.lockReason != plan.target.lockReason ||
			currentEntry.prunable != plan.target.prunable || currentEntry.pruneReason != plan.target.pruneReason {
			facts.LockPrune = worktreeremove.InvariantChanged
		}
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("registered target identity changed")}
	}
	facts.Registration = worktreeremove.InvariantStable
	facts.LockPrune = worktreeremove.InvariantStable
	facts.TargetPath = worktreeremove.InvariantStable

	currentTarget, err := inspectWorktreeTarget(ctx, plan.git, currentEntry)
	if err != nil {
		return worktreeRevalidationObservation{facts: facts, err: err}
	}
	if !sameWorktreePath(currentTarget.gitDir, plan.target.gitDir) {
		facts.GitAdminDirectory = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target git directory changed")}
	}
	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")}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-run `bd worktree remove` to build a fresh plan reflecting the new registry state
  2. Check `git worktree list --porcelain` and compare lock/prune flags; unlock with `git worktree unlock <path>` if locking blocks the removal
  3. Avoid concurrent lock/unlock/prune operations on the same worktree while a removal is in flight

Example fix

// before: someone locked the worktree mid-plan
$ bd worktree remove feature
// error: registered target identity changed
// after
$ git worktree unlock /wt/feature
$ bd worktree remove feature   # rebuild plan against current state
Defensive patterns

Strategy: retry

Validate before calling

git worktree list --porcelain | grep -A3 '^worktree /wt/feature$' | grep -q '^locked' && { echo 'worktree locked; unlock first'; exit 1; }

Type guard

func identityStable(before, after worktreeEntry) bool {
	return before.locked == after.locked && before.lockReason == after.lockReason &&
		before.prunable == after.prunable && before.pruneReason == after.pruneReason
}

Try / catch

obs := revalidateWorktreePlan(ctx, plan)
if obs.err != nil && strings.Contains(obs.err.Error(), "identity changed") {
	plan, err = buildWorktreeRemovalPlan(ctx, git, name, nil) // rebuild and retry once
}

Prevention

When it happens

Trigger: `git worktree lock`/`unlock`/`prune` or `bd worktree lock/unlock` executed on the target between building the removal plan and executing it; another process altering the worktree's prune state (e.g. the directory temporarily disappearing and reappearing).

Common situations: A teammate locked the worktree while you were removing it; an IDE or agent locked worktrees automatically; a prune-capable state appeared because the directory was briefly renamed or unmounted.

Related errors


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