gastownhall/beads · error

failed to write pre-commit hook: %w

Error message

failed to write pre-commit hook: %w

What it means

installGitHooks writes the pre-commit hook script to the hooks directory with os.WriteFile(..., 0700). A write failure is wrapped with this message. The hook is not installed, so bd's pre-commit automation will not run.

Source

Thrown at cmd/bd/init_git_hooks.go:191

	// pre-commit hook
	preCommitPath := filepath.Join(hooksDir, "pre-commit")
	preCommitContent := buildPreCommitHook(chainHooks, existingHooks)

	// post-merge hook
	postMergePath := filepath.Join(hooksDir, "post-merge")
	postMergeContent := buildPostMergeHook(chainHooks, existingHooks)

	// Normalize line endings to LF — on Windows/NTFS, Go string literals
	// are fine but concatenated content from other sources may have CRLF.
	// Git hooks with CRLF fail: /usr/bin/env: 'sh\r': No such file or directory
	preCommitContent = strings.ReplaceAll(preCommitContent, "\r\n", "\n")
	postMergeContent = strings.ReplaceAll(postMergeContent, "\r\n", "\n")

	// Write pre-commit hook (executable scripts need 0700)
	// #nosec G306 - git hooks must be executable
	if err := os.WriteFile(preCommitPath, []byte(preCommitContent), 0700); err != nil {
		return fmt.Errorf("failed to write pre-commit hook: %w", err)
	}

	// Write post-merge hook (executable scripts need 0700)
	// #nosec G306 - git hooks must be executable
	if err := os.WriteFile(postMergePath, []byte(postMergeContent), 0700); err != nil {
		return fmt.Errorf("failed to write post-merge hook: %w", err)
	}

	if chainHooks {
		fmt.Printf("%s Chained bd hooks with existing hooks\n", ui.RenderPass("✓"))
	}

	return nil
}

// buildPreCommitHook generates the pre-commit hook content using section markers (GH#1380).
// If chainHooks is true, chained hooks (.old) are called before the beads section.
func buildPreCommitHook(chainHooks bool, existingHooks []hookInfo) string {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check permissions on .git/hooks and any existing pre-commit file; remove or chown conflicting files: `ls -la .git/hooks/pre-commit`.
  2. Free disk space if the filesystem is full.
  3. Re-run `bd init` after fixing the environment.
  4. Verify the written hook: `.git/hooks/pre-commit` should exist and be executable.

Example fix

// before
$ ls -la .git/hooks/pre-commit
-rwxr-xr-x 1 root root ... (root-owned)
// after
$ sudo rm .git/hooks/pre-commit
$ bd init
Defensive patterns

Strategy: validation

Validate before calling

pc := filepath.Join(hooksDir, "pre-commit")
if fi, err := os.Stat(pc); err == nil && (!fi.Mode().Perm()&0200 != 0 || isForeignOwner(fi)) {
    return fmt.Errorf("%s exists and is not overwritable", pc)
}

Try / catch

if err := installGitHooks(hooksDir); err != nil {
    if strings.Contains(err.Error(), "failed to write pre-commit hook") {
        var pe *fs.PathError
        if errors.As(err, &pe) { /* handle pe.Err: EACCES -> fix perms; ENOSPC -> free disk */ }
    }
    return err
}

Prevention

When it happens

Trigger: os.WriteFile(preCommitPath, ...) errors — permission denied on .git/hooks, disk full, hooks dir deleted between MkdirAll and write, or an unwritable existing pre-commit file owned by another user.

Common situations: Shared repos where a root-owned pre-commit hook already exists, container/CI environments with read-only .git, or anti-virus/monitoring locking the hook file on Windows.

Related errors


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