gastownhall/beads · error

failed to write post-merge hook: %w

Error message

failed to write post-merge hook: %w

What it means

The sibling of the pre-commit write: installGitHooks writes the post-merge hook with os.WriteFile(postMergePath, ..., 0700) and wraps any failure with this message. The pre-commit hook may already have been written, leaving a partially installed hook set.

Source

Thrown at cmd/bd/init_git_hooks.go:197

	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 {
	section := generateHookSection("pre-commit")

	if chainHooks {
		var existingPreCommit string
		for _, hook := range existingHooks {
			if hook.name == "pre-commit" && hook.exists && !hook.isBdHook {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect and remove/fix the conflicting `.git/hooks/post-merge` file, then re-run `bd init`.
  2. Check .git/hooks directory permissions and free disk space.
  3. Re-run `bd init` to install both hooks consistently (pre-commit may need rewriting too).
  4. Confirm both hooks exist and are executable before committing.

Example fix

// before
$ ls -la .git/hooks
pre-commit (new, ok)  post-merge: permission denied
// after
$ rm -f .git/hooks/post-merge
$ bd init
Defensive patterns

Strategy: validation

Validate before calling

pm := filepath.Join(hooksDir, "post-merge")
if fi, err := os.Stat(pm); err == nil && fi.Mode().Perm()&0200 == 0 {
    return fmt.Errorf("%s exists and is not overwritable", pm)
}

Try / catch

if err := installGitHooks(hooksDir); err != nil {
    if strings.Contains(err.Error(), "failed to write post-merge hook") {
        // pre-commit may already be installed; fix post-merge and re-run init
    }
    return err
}

Prevention

When it happens

Trigger: os.WriteFile(postMergePath, ...) errors — same causes as pre-commit: permission denied, read-only filesystem, disk full, or an unwritable existing post-merge file.

Common situations: Existing root-owned or non-writable post-merge hook in shared repos, CI environments mounting .git read-only, or transient IO errors mid-install.

Related errors


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