gastownhall/beads · error

failed to create hooks directory: %w

Error message

failed to create hooks directory: %w

What it means

bd could not create the hooks directory (e.g. .git/hooks, .beads/hooks, or .beads-hooks) via os.MkdirAll, so hook installation aborts. The underlying OS error is wrapped (%w), so the real cause — permissions, a file blocking the path, read-only filesystem — is in the chained message.

Source

Thrown at cmd/bd/hooks.go:902

		}
	} else {
		// Use common git directory for hooks (shared across worktrees)
		var err error
		hooksDir, err = git.GetGitHooksDir()
		if err != nil {
			return err
		}
	}

	// Create hooks directory if it doesn't exist.
	// Directories inside .beads/ use BeadsDirPerm (0700); git-managed hook
	// dirs (.git/hooks, .beads-hooks) use 0755 so git can execute them.
	hooksDirPerm := os.FileMode(0755)
	if beadsHooks {
		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)
		}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped OS error to identify the cause (permission denied vs file exists vs read-only)
  2. Remove/rename any regular file occupying the hooks directory path, then re-run
  3. Fix directory ownership/permissions (e.g. `sudo chown -R $(whoami) .git .beads`) and re-run
  4. If the filesystem is read-only, install hooks from a writable checkout

Example fix

// before: file blocks directory
$ ls .beads/hooks  # -rw-r--r-- regular file
// after
$ mv .beads/hooks .beads/hooks.old && bd hooks install
Defensive patterns

Strategy: validation

Validate before calling

dir="$(dirname "$target")"; if [ -e "$dir" ] && [ ! -d "$dir" ]; then echo "$dir exists and is not a directory"; fi; [ -w "$dir" ] || echo "$dir not writable"

Try / catch

if err := installHooks(...); err != nil { var pe *fs.PathError; if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EACCES) { /* fix permissions and retry */ } }

Prevention

When it happens

Trigger: installHooksWithOptions calls os.MkdirAll(hooksDir, perm) and the OS returns an error: parent dir not writable, a regular file already exists at hooksDir, full disk, or read-only mount (e.g. container image filesystem).

Common situations: A stray file named `hooks` exists inside .beads/ or .git/; running bd as a non-root user in a root-owned checkout; Docker/npm-ci environments mounting the repo read-only.

Related errors


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