gastownhall/beads · error

target git directory identity changed

Error message

target git directory identity changed

What it means

This error is raised by `bd worktree remove` during TOCTOU revalidation: just before executing a pre-built removal plan, bd re-inspects the worktree's .git admin directory and compares it against the FileInfo pinned at plan time. `os.SameFile` fails (the inode changed — the directory was deleted/recreated or replaced) or the pinned mode/size/mtime differ. bd aborts the removal because the filesystem object it planned to remove is not the same one that still exists, so removing it could destroy different data than the user approved.

Source

Thrown at cmd/bd/worktree_cmd.go:1455

	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")}
	}
	facts.GitMarker = worktreeremove.InvariantStable
	if currentTarget.gitDirFingerprint != plan.target.gitDirFingerprint {
		facts.GitAdminDirectoryBytes = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("target git directory identity changed (contents mismatch)")}
	}
	facts.GitAdminDirectoryBytes = worktreeremove.InvariantStable
	if currentTarget.gitMarkerFingerprint != plan.target.gitMarkerFingerprint {
		facts.GitMarkerBytes = worktreeremove.InvariantChanged
		return worktreeRevalidationObservation{facts: facts, err: fmt.Errorf("registered target identity changed (git marker mismatch)")}
	}
	facts.GitMarkerBytes = worktreeremove.InvariantStable

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-run `bd worktree remove` so a fresh plan is built against the current state of the worktree
  2. Investigate what concurrently touched the worktree (other agents, CI, sync tools) and serialize worktree operations
  3. Verify the admin directory with `git rev-parse --git-dir` / `ls -i` to confirm the directory was replaced; if data loss is possible, inspect before removing
  4. If replacement is expected (e.g. worktree was intentionally recreated), remove the new worktree with a new command rather than forcing the stale plan

Example fix

// before: reusing a stale plan captured earlier
plan := buildRemovalPlan(target)
// ...long-running steps that let the worktree be replaced...
executeRemoval(plan) // fails: target git directory identity changed
// after: rebuild the plan immediately before executing
plan := buildRemovalPlan(target)
executeRemoval(plan) // plan and execution are adjacent, window minimized
Defensive patterns

Strategy: retry

Validate before calling

// Before relying on a removal plan, confirm the admin dir is unchanged:
planInfo, err := os.Stat(filepath.Join(worktreePath, ".git"))
if err != nil { return err }
if !os.SameFile(planInfo, pinned.gitDirInfo) ||
   planInfo.Mode() != pinned.gitDirInfo.Mode() ||
   planInfo.Size() != pinned.gitDirInfo.Size() {
    return fmt.Errorf("admin dir replaced; rebuild plan")
}

Type guard

func sameFileIdentity(current, pinned os.FileInfo) bool {
    return os.SameFile(current, pinned) &&
        current.Mode() == pinned.Mode() &&
        current.Size() == pinned.Size() &&
        current.ModTime().Equal(pinned.ModTime())
}

Try / catch

err := bdWorktreeRemove(path)
if err != nil && strings.Contains(err.Error(), "git directory identity changed") {
    // plan is stale: rebuild and retry once
    err = rebuildPlanAndRemove(path)
}

Prevention

When it happens

Trigger: Running `bd worktree remove <path>` where, between plan construction and revalidation, the worktree's .git file/admin directory is replaced: the worktree was deleted and re-created, `git worktree repair`/`git worktree move` re-pointed the admin directory, a backup/restore swapped the directory, or another process recreated the path with a new inode or touched its metadata.

Common situations: Another agent or CI job concurrently runs `git worktree remove`/`add` on the same path; an editor or sync tool (Dropbox, rsync) rewrites the directory; `git worktree move` relocates the admin dir; the plan was captured seconds earlier in a script and the worktree was refreshed in between.

Related errors


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