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

  1. Read the wrapped (%w) cause to see whether it is a symlink or tracked-file refusal
  2. 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
  3. If git-tracked: `git rm --cached <hook>` or move hooks to an untracked directory, then re-run
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/de192917322800e4. Report an issue: GitHub.