{"record":{"id":"2fc45ae0820b057c","repo":"gastownhall/beads","slug":"target-git-directory-identity-changed","errorCode":null,"errorMessage":"target git directory identity changed","messagePattern":"target git directory identity changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/worktree_cmd.go","lineNumber":1455,"sourceCode":"\tif currentTarget.statusFingerprint != plan.target.statusFingerprint {\n\t\tfacts.DirtyFileFingerprint = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target changed files changed\")}\n\t}\n\tfacts.DirtyFileFingerprint = worktreeremove.InvariantStable\n\tif !plan.force && currentTarget.status != \"\" {\n\t\tfacts.Cleanliness = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target is no longer clean\")}\n\t}\n\tif !os.SameFile(currentTarget.pathInfo, plan.target.pathInfo) ||\n\t\t!samePinnedFileMetadata(currentTarget.pathInfo, plan.target.pathInfo) {\n\t\tfacts.TargetDirectory = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target directory identity changed\")}\n\t}\n\tfacts.TargetDirectory = worktreeremove.InvariantStable\n\tif !os.SameFile(currentTarget.gitDirInfo, plan.target.gitDirInfo) ||\n\t\t!samePinnedFileMetadata(currentTarget.gitDirInfo, plan.target.gitDirInfo) {\n\t\tfacts.GitAdminDirectory = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target git directory identity changed\")}\n\t}\n\tfacts.GitAdminDirectory = worktreeremove.InvariantStable\n\tif !os.SameFile(currentTarget.gitMarkerInfo, plan.target.gitMarkerInfo) ||\n\t\t!samePinnedFileMetadata(currentTarget.gitMarkerInfo, plan.target.gitMarkerInfo) {\n\t\tfacts.GitMarker = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target git marker identity changed\")}\n\t}\n\tfacts.GitMarker = worktreeremove.InvariantStable\n\tif currentTarget.gitDirFingerprint != plan.target.gitDirFingerprint {\n\t\tfacts.GitAdminDirectoryBytes = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"target git directory identity changed (contents mismatch)\")}\n\t}\n\tfacts.GitAdminDirectoryBytes = worktreeremove.InvariantStable\n\tif currentTarget.gitMarkerFingerprint != plan.target.gitMarkerFingerprint {\n\t\tfacts.GitMarkerBytes = worktreeremove.InvariantChanged\n\t\treturn worktreeRevalidationObservation{facts: facts, err: fmt.Errorf(\"registered target identity changed (git marker mismatch)\")}\n\t}\n\tfacts.GitMarkerBytes = worktreeremove.InvariantStable","sourceCodeStart":1437,"sourceCodeEnd":1473,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/worktree_cmd.go#L1437-L1473","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: reusing a stale plan captured earlier\nplan := buildRemovalPlan(target)\n// ...long-running steps that let the worktree be replaced...\nexecuteRemoval(plan) // fails: target git directory identity changed\n// after: rebuild the plan immediately before executing\nplan := buildRemovalPlan(target)\nexecuteRemoval(plan) // plan and execution are adjacent, window minimized","handlingStrategy":"retry","validationCode":"// Before relying on a removal plan, confirm the admin dir is unchanged:\nplanInfo, err := os.Stat(filepath.Join(worktreePath, \".git\"))\nif err != nil { return err }\nif !os.SameFile(planInfo, pinned.gitDirInfo) ||\n   planInfo.Mode() != pinned.gitDirInfo.Mode() ||\n   planInfo.Size() != pinned.gitDirInfo.Size() {\n    return fmt.Errorf(\"admin dir replaced; rebuild plan\")\n}","typeGuard":"func sameFileIdentity(current, pinned os.FileInfo) bool {\n    return os.SameFile(current, pinned) &&\n        current.Mode() == pinned.Mode() &&\n        current.Size() == pinned.Size() &&\n        current.ModTime().Equal(pinned.ModTime())\n}","tryCatchPattern":"err := bdWorktreeRemove(path)\nif err != nil && strings.Contains(err.Error(), \"git directory identity changed\") {\n    // plan is stale: rebuild and retry once\n    err = rebuildPlanAndRemove(path)\n}","preventionTips":["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"],"tags":["git","worktree","concurrency","filesystem","race-condition"],"backgroundTag":"worktree-state-changed-underneath","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}