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.InvariantStableView on GitHub (pinned to 71377f2769)
Solutions
- Re-run `bd worktree remove` so a fresh plan is built against the current state of the worktree
- Investigate what concurrently touched the worktree (other agents, CI, sync tools) and serialize worktree operations
- 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
- 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
- Build the removal plan and execute it back-to-back; never hold a plan across long-running steps
- Serialize worktree operations — don't let agents/CI touch the same worktree concurrently
- Avoid tools that replace directories in place (rsync --delete, restore jobs) while removal is pending
- After any git worktree repair/move, always rebuild the plan before removing
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
- target git marker identity changed
- target HEAD disagrees with git worktree registry (registry %
- target git directory is not a real directory: %s
- failed to pin target git marker identity: %w
- cannot safely prepare .gitignore cleanup: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/2fc45ae0820b057c.
Report an issue: GitHub.