gastownhall/beads · error
refusing to install %s hook: %w
Error message
refusing to install %s hook: %w
What it means
installHooksWithOptions pre-flights every target hook through guardHookWritePath and refuses the entire install if any is unsafe (symlinked, git-tracked and foreign, etc.), so it never leaves hooks half-installed. This error wraps one of those guard errors — notably error 750's tracked-hook refusal or the symlink refusal.
Source
Thrown at cmd/bd/hooks.go:917
hooksDirPerm = config.BeadsDirPerm
}
if err := os.MkdirAll(hooksDir, hooksDirPerm); err != nil {
return fmt.Errorf("failed to create hooks directory: %w", err)
}
// When setting a local core.hooksPath (beads or shared mode), preserve any
// hooks from the previously effective hooks directory (e.g. a global
// core.hooksPath or the default .git/hooks). Without this, setting a local
// core.hooksPath silently shadows the global one and those hooks stop running.
if beadsHooks || shared {
preservePreexistingHooks(hooksDir)
}
// Refuse the whole install up front if any target is unsafe to write —
// stopping midway through the loop would leave hooks half-installed.
for _, hookName := range hookNames {
if err := guardHookWritePath(filepath.Join(hooksDir, hookName), shared); err != nil {
return fmt.Errorf("refusing to install %s hook: %w", hookName, err)
}
}
// Install each hook using section markers (GH#1380).
// Only the content between markers is managed by beads; user content
// outside the markers is preserved across reinstalls and upgrades.
for _, hookName := range hookNames {
hookPath := filepath.Join(hooksDir, hookName)
section := generateHookSection(hookName)
// Read existing hook file (if any)
// #nosec G304 -- hook path constrained to hooks directory
existing, readErr := os.ReadFile(hookPath)
if readErr != nil && !os.IsNotExist(readErr) {
return fmt.Errorf("failed to read %s: %w", hookName, readErr)
}
View on GitHub (pinned to 71377f2769)
Solutions
- Read the wrapped (%w) cause to see whether it is a symlink or tracked-file refusal
- If symlinked (e.g. by husky): remove the symlink or configure bd's hooks directory (.beads-hooks / .beads/hooks) so it does not clash with the other tool
- If git-tracked: `git rm --cached <hook>` or move hooks to an untracked directory, then re-run
- Re-run `bd hooks install` once all targets are safe
Example fix
// before: husky symlink $ ls -l .git/hooks/pre-commit -> ../../.husky/pre-commit // after $ rm .git/hooks/pre-commit && bd hooks install
Defensive patterns
Strategy: validation
Validate before calling
for h in pre-commit pre-push; do p=".git/hooks/$h"; [ -L "$p" ] && echo "$p is a symlink"; git ls-files --error-unmatch "$p" 2>/dev/null && echo "$p is tracked"; done
Type guard
func canInstallHook(path string) bool { fi, err := os.Lstat(path); if err != nil { return true }; return fi.Mode()&os.ModeSymlink == 0 && (!isGitTrackedFile(path) || isBdOwnedHookFile(path)) } Try / catch
if err := installHooksWithOptions(names, force, shared, chain, beads); err != nil { if strings.Contains(err.Error(), "refusing to install") { log.Warn("hook target unsafe; resolve wrapped cause first", "cause", err) }; return err } Prevention
- Remove husky/other-manager symlinks before bd installs, or point bd at its own hooks dir
- Don't commit hook files bd will write
- Pre-run guardHookWritePath logic (or `bd hooks install --dry-run` if available) in scripts
When it happens
Trigger: `bd hooks install` where any target (pre-commit, pre-push, ...) resolves to a symlink, or to a git-tracked file that is not bd-owned.
Common situations: frameworks like husky symlink .git/hooks into their own directory; a custom hook was committed to git; switching hook managers without cleaning up old symlinks.
Related errors
- %s is tracked by git and not a bd-managed hook; bd will not
- not a git repository
- failed to install hooks: %w
- failed to create hooks directory: %w
- failed to write pre-commit hook: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/de192917322800e4.
Report an issue: GitHub.