{"record":{"id":"da0bc7fc99bbeb06","repo":"gastownhall/beads","slug":"s-is-a-symlink-to-s-writing-would-rewrite-the-l","errorCode":null,"errorMessage":"%s is a symlink to %s; writing would rewrite the link target, not the hook\nRemove the symlink (or leave that hook to its owner) and re-run","messagePattern":"(.+?) is a symlink to (.+?); writing would rewrite the link target, not the hook\nRemove the symlink \\(or leave that hook to its owner\\) and re-run","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/hooks.go","lineNumber":827,"sourceCode":"//   - a hook file tracked by git: writing it dirties the working tree.\n//     allowTracked exempts shared installs (.beads-hooks/ is deliberately\n//     committed).\nfunc guardHookWritePath(hookPath string, allowTracked bool) error {\n\tfi, err := os.Lstat(hookPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil // creating a new file — nothing to clobber\n\t\t}\n\t\treturn fmt.Errorf(\"failed to stat %s: %w\", hookPath, err)\n\t}\n\tif fi.Mode()&os.ModeSymlink != 0 {\n\t\ttarget := \"unresolvable target\"\n\t\tif resolved, rerr := filepath.EvalSymlinks(hookPath); rerr == nil {\n\t\t\ttarget = resolved\n\t\t} else if link, lerr := os.Readlink(hookPath); lerr == nil {\n\t\t\ttarget = link\n\t\t}\n\t\treturn fmt.Errorf(\"%s is a symlink to %s; writing would rewrite the link target, not the hook\\nRemove the symlink (or leave that hook to its owner) and re-run\", hookPath, target)\n\t}\n\tif allowTracked {\n\t\treturn nil\n\t}\n\t// A tracked file is refused only when bd does NOT own it: writing into a\n\t// foreign tracked file dirties every clone that shares it (the wy-81fnur\n\t// incident). A bd-owned hook the user chose to commit (e.g. a team-shared\n\t// .beads/hooks/) is bd's to maintain — same policy as shared installs.\n\tif isGitTrackedFile(hookPath) && !isBdOwnedHookFile(hookPath) {\n\t\treturn fmt.Errorf(\"%s is tracked by git and not a bd-managed hook; bd will not modify committed files it does not own\\nUntrack it (git rm --cached) or move hooks to an untracked directory and re-run\", hookPath)\n\t}\n\treturn nil\n}\n\n// isBdOwnedHookFile reports whether the hook file at path is bd-managed:\n// either section-marker format or a legacy bd hook (shim or inline).\nfunc isBdOwnedHookFile(path string) bool {\n\tcontent, err := os.ReadFile(path) // #nosec G304 -- path is a hook location bd resolved","sourceCodeStart":809,"sourceCodeEnd":845,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/hooks.go#L809-L845","documentation":"Returned by guardHookWritePath when the hook path is a symlink: writing to it would overwrite the link's target file (owned by whoever set up the link) rather than the hook itself, so bd refuses. This protects foreign tools (e.g. other hook managers) that own the real file.","triggerScenarios":"installHooksWithOptions or applyHookMigrationExecution finds the hook path is a symlink (Lstat ModeSymlink set); the message includes the resolved target via filepath.EvalSymlinks, or the raw link via os.Readlink if unresolvable.","commonSituations":"A hook manager (husky, pre-commit, lefthook) or dotfiles setup symlinked .git/hooks/* into a shared config directory; bd then tries to install its hooks over those links.","solutions":["Remove the symlink (rm .git/hooks/<hook>) and re-run bd init / bd hooks install so bd owns a real file.","Leave that particular hook to its owner (skip installing bd's hook there) if another tool manages it.","Configure the other hook manager to chain to bd's hook instead of linking the file."],"exampleFix":"// before\n$ ls -l .git/hooks/pre-commit -> /dotfiles/pre-commit\n$ bd hooks install\n// error: .git/hooks/pre-commit is a symlink to /dotfiles/pre-commit ...\n// after\n$ rm -f .git/hooks/pre-commit\n$ bd hooks install","handlingStrategy":"validation","validationCode":"if fi, err := os.Lstat(hookPath); err == nil && fi.Mode()&os.ModeSymlink != 0 {\n\treturn errors.New(hookPath + \" is a symlink; resolve before installing bd hooks\")\n}","typeGuard":null,"tryCatchPattern":"if err := guardHookWritePath(hookPath, allowTracked); err != nil {\n\tif strings.Contains(err.Error(), \"is a symlink to\") {\n\t\t// skip this hook or prompt the user to remove the link\n\t}\n}","preventionTips":["Check for symlinked hooks (ls -l .git/hooks) before running bd init in repos managed by husky/pre-commit/lefthook/dotfiles.","Let the owning hook manager chain to bd's hook instead of linking files.","Pass allowTracked only when bd genuinely owns the tracked file."],"tags":["git-hooks","symlink","safety-guard"],"backgroundTag":"symlink-write-refused","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}