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
- Replace the symlink with a real file at the hook path, then re-run migration
- Check `ls -la .git/hooks/` for symlinks and convert them
- Re-run apply after removing whatever occupies the path
- 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
- Use real files, not symlinks, in .git/hooks
- Check `ls -la .git/hooks/` after any dotfile-manager sync
- Re-run plan after changing hook paths so targets are re-validated
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
- resolve path: %w
- remote target %s is non-empty but is neither a bare git repo
- failed to stat %s: %w
- %s is a symlink to %s; writing would rewrite the link target
- failed to remove %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0b25c4821616f3ca.
Report an issue: GitHub.