{"record":{"id":"0b25c4821616f3ca","repo":"gastownhall/beads","slug":"refusing-to-write-migrated-hook-s-w","errorCode":null,"errorMessage":"refusing to write migrated hook %s: %w","messagePattern":"refusing to write migrated hook (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/migrate_hooks_apply.go","lineNumber":261,"sourceCode":"\n\tpreparedWrites, err := prepareHookMigrationWrites(execPlan.WriteOps)\n\tif err != nil {\n\t\treturn hookMigrationApplySummary{}, err\n\t}\n\n\tif err := validateRetireCollisionPolicy(execPlan.RetireOps); err != nil {\n\t\treturn hookMigrationApplySummary{}, err\n\t}\n\n\tsummary := hookMigrationApplySummary{\n\t\tWrittenHooks:     make([]string, 0, len(preparedWrites)),\n\t\tRetiredArtifacts: make([]string, 0, len(execPlan.RetireOps)),\n\t\tSkippedArtifacts: make([]string, 0),\n\t}\n\n\tfor _, write := range preparedWrites {\n\t\tif err := guardHookWritePath(write.Path, false); err != nil {\n\t\t\treturn summary, fmt.Errorf(\"refusing to write migrated hook %s: %w\", write.HookName, err)\n\t\t}\n\t\t// #nosec G306 -- git hooks must be executable for Git to run them\n\t\tif err := os.WriteFile(write.Path, write.Content, 0755); err != nil {\n\t\t\treturn summary, fmt.Errorf(\"writing migrated hook %s: %w\", write.Path, err)\n\t\t}\n\t\tsummary.WrittenHooks = append(summary.WrittenHooks, write.HookName)\n\t}\n\n\tfor _, retire := range execPlan.RetireOps {\n\t\tretired, retiredErr := retireHookSidecar(retire)\n\t\tif retiredErr != nil {\n\t\t\treturn summary, retiredErr\n\t\t}\n\t\tif retired == \"\" {\n\t\t\tsummary.SkippedArtifacts = append(summary.SkippedArtifacts, retire.SourcePath)\n\t\t\tcontinue\n\t\t}\n\t\tsummary.RetiredArtifacts = append(summary.RetiredArtifacts, retired)","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/migrate_hooks_apply.go#L243-L279","documentation":"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.","triggerScenarios":"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.","commonSituations":"Dotfile managers symlinking .git/hooks; a race where another tool rewrote the hook mid-migration; read-only or unusual filesystems rejecting the operation.","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"],"exampleFix":"// before\n.git/hooks/pre-commit -> /etc/alternatives/git-pre-commit  (symlink)\n// after\nrm .git/hooks/pre-commit && install -m 0755 /etc/alternatives/git-pre-commit .git/hooks/pre-commit","handlingStrategy":"validation","validationCode":"// pre-check the target before migration\nconst st = fs.lstatSync(hookPath);\nif (st.isSymbolicLink()) throw new Error('replace symlink with a real file before migrating');","typeGuard":"func isRegularFile(p string) bool { fi, err := os.Lstat(p); return err == nil && fi.Mode().IsRegular() }","tryCatchPattern":"if err := apply(...); err != nil && strings.Contains(err.Error(), \"refusing to write migrated hook\") {\n    // inspect the path named in the error, replace symlink, retry\n}","preventionTips":["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"],"tags":["git-hooks","symlink","safety","filesystem"],"backgroundTag":"unsafe-write-path","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}