gastownhall/beads · error

refusing to write migrated hook %s: %w

Error message

refusing to write migrated hook %s: %w

What it means

Before writing each migrated hook file, bd calls guardHookWritePath to ensure the target is a safe regular file (not a symlink, not a directory, not otherwise hostile). If the guard rejects the path, the write of that migrated hook is refused and the underlying guard error is wrapped with the hook name for context.

Source

Thrown at cmd/bd/migrate_hooks_apply.go:261

	preparedWrites, err := prepareHookMigrationWrites(execPlan.WriteOps)
	if err != nil {
		return hookMigrationApplySummary{}, err
	}

	if err := validateRetireCollisionPolicy(execPlan.RetireOps); err != nil {
		return hookMigrationApplySummary{}, err
	}

	summary := hookMigrationApplySummary{
		WrittenHooks:     make([]string, 0, len(preparedWrites)),
		RetiredArtifacts: make([]string, 0, len(execPlan.RetireOps)),
		SkippedArtifacts: make([]string, 0),
	}

	for _, write := range preparedWrites {
		if err := guardHookWritePath(write.Path, false); err != nil {
			return summary, fmt.Errorf("refusing to write migrated hook %s: %w", write.HookName, err)
		}
		// #nosec G306 -- git hooks must be executable for Git to run them
		if err := os.WriteFile(write.Path, write.Content, 0755); err != nil {
			return summary, fmt.Errorf("writing migrated hook %s: %w", write.Path, err)
		}
		summary.WrittenHooks = append(summary.WrittenHooks, write.HookName)
	}

	for _, retire := range execPlan.RetireOps {
		retired, retiredErr := retireHookSidecar(retire)
		if retiredErr != nil {
			return summary, retiredErr
		}
		if retired == "" {
			summary.SkippedArtifacts = append(summary.SkippedArtifacts, retire.SourcePath)
			continue
		}
		summary.RetiredArtifacts = append(summary.RetiredArtifacts, retired)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Replace the symlink with a real file at the hook path, then re-run migration
  2. Check `ls -la .git/hooks/` for symlinks and convert them
  3. Re-run apply after removing whatever occupies the path
  4. If the guard error is permissions-related, fix ownership/permissions on the hook path

Example fix

// before
.git/hooks/pre-commit -> /etc/alternatives/git-pre-commit  (symlink)
// after
rm .git/hooks/pre-commit && install -m 0755 /etc/alternatives/git-pre-commit .git/hooks/pre-commit
Defensive patterns

Strategy: validation

Validate before calling

// pre-check the target before migration
const st = fs.lstatSync(hookPath);
if (st.isSymbolicLink()) throw new Error('replace symlink with a real file before migrating');

Type guard

func isRegularFile(p string) bool { fi, err := os.Lstat(p); return err == nil && fi.Mode().IsRegular() }

Try / catch

if err := apply(...); err != nil && strings.Contains(err.Error(), "refusing to write migrated hook") {
    // inspect the path named in the error, replace symlink, retry
}

Prevention

When it happens

Trigger: guardHookWritePath(write.Path, false) returned an error right before os.WriteFile — typically because the hook path is a symlink or was replaced by a non-regular file between planning and writing.

Common situations: Dotfile managers symlinking .git/hooks; a race where another tool rewrote the hook mid-migration; read-only or unusual filesystems rejecting the operation.

Related errors


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