{"record":{"id":"4972a507440f7bf2","repo":"gastownhall/beads","slug":"target-head-disagrees-with-git-worktree-registry","errorCode":null,"errorMessage":"target HEAD disagrees with git worktree registry (registry %q, target %q)","messagePattern":"target HEAD disagrees with git worktree registry \\(registry %q, target %q\\)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/worktree_cmd.go","lineNumber":998,"sourceCode":"\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\"target HEAD does not resolve to a commit: %w\", err)\n\t}\n\tstatusOutput, err := git.output(\n\t\tctx,\n\t\tworktree.path,\n\t\t\"status\",\n\t\t\"--porcelain=v1\",\n\t\t\"-z\",\n\t\t\"--untracked-files=all\",\n\t\t\"--ignore-submodules=none\",\n\t\t\"--ignored=matching\",\n\t)\n\tif err != nil {\n\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\"failed to inspect target cleanliness: %w\", err)\n\t}\n\n\theadOID := strings.TrimSpace(string(headOutput))\n\tif headOID == \"\" || headOID != worktree.headOID {\n\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\n\t\t\t\"target HEAD disagrees with git worktree registry (registry %q, target %q)\",\n\t\t\tworktree.headOID,\n\t\t\theadOID,\n\t\t)\n\t}\n\tpathInfo, err := os.Lstat(worktree.path)\n\tif err != nil {\n\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\"failed to pin target directory identity: %w\", err)\n\t}\n\tif pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.IsDir() {\n\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\"target path is not a real directory: %s\", worktree.path)\n\t}\n\tgitDirInfo, err := os.Lstat(gitDir)\n\tif err != nil {\n\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\"failed to pin target git directory identity: %w\", err)\n\t}\n\tif gitDirInfo.Mode()&os.ModeSymlink != 0 || !gitDirInfo.IsDir() {\n\t\treturn pinnedWorktreeTarget{}, fmt.Errorf(\"target git directory is not a real directory: %s\", gitDir)","sourceCodeStart":980,"sourceCodeEnd":1016,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/worktree_cmd.go#L980-L1016","documentation":"inspectWorktreeTarget compares the HEAD commit observed in the target worktree with the headOID recorded in bd's worktree registry. If they differ (or HEAD is empty), removal is refused. This prevents removing a worktree whose state changed since registration, guarding against racing mutations.","triggerScenarios":"The worktree's HEAD moved (new commits, branch switch, reset) between registration and the removal call; a stale registry entry; observeRevalidation detects drift during a post-confirmation re-check.","commonSituations":"Someone committed or switched branches while the removal prompt was open; a CI job or agent pushed/reset the branch concurrently; registry data copied from another machine with a different HEAD; registry out of sync after a manual `git worktree add`.","solutions":["Check current HEAD with `git -C <path> rev-parse HEAD` and refresh/re-register the worktree entry so the registry matches, then retry.","If the movement was intentional, re-run `bd worktree remove` so a fresh inspection pins the new HEAD.","Quiesce any process mutating the worktree (stop CI/agents) before removing.","If the registry entry is stale or corrupt, remove it and re-register from `git worktree list`."],"exampleFix":"// before\nbd worktree remove feature-x\n// error: target HEAD disagrees with git worktree registry (registry \"abc123\", target \"def456\")\n// after\ngit -C /repo/.worktrees/feature-x rev-parse HEAD   # confirm intended state\nbd worktree remove feature-x                        # re-run with refreshed registry","handlingStrategy":"retry","validationCode":"const head = Bun.spawnSync([\"git\", \"-C\", path, \"rev-parse\", \"HEAD\"]).stdout.toString().trim();\nconst reg = JSON.parse(Bun.spawnSync([\"bd\", \"worktree\", \"list\", \"--json\"]).stdout.toString());\nconst entry = reg.find(w => w.path === path);\nif (entry && entry.headOid && entry.headOid !== head) throw new Error(\"registry drift — refresh before removing\");","typeGuard":null,"tryCatchPattern":"try {\n  await bd(\"worktree\", \"remove\", path);\n} catch (e) {\n  if (String(e.message).includes(\"target HEAD disagrees\")) {\n    await bd(\"worktree\", \"remove\", path); // re-run: fresh inspection pins new HEAD\n  } else throw e;\n}","preventionTips":["Don't mutate the worktree (commit/switch/reset) while a removal is in flight or a prompt is open.","Stop CI jobs/agents operating on the worktree before removing.","Refresh the registry after intentional HEAD changes.","Avoid copying registry state between machines."],"tags":["worktree","git","registry-drift","race-condition"],"backgroundTag":"worktree-state-drift","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}