@@ -508,6 +508,31 @@ func TestEnsurePreMergeCommitHook_OverwritesManagedHook(t *testing.T) {
508508 assert .Contains (t , string (data ), "'PLAN.md'" )
509509}
510510
511+ func TestEnsurePreMergeCommitHook_SetsExecutableBitOnExistingHook (t * testing.T ) {
512+ if runtime .GOOS == "windows" {
513+ t .Skip ("POSIX permission semantics not applicable on Windows" )
514+ }
515+ dir := t .TempDir ()
516+ hooksDir := filepath .Join (dir , ".git" , "hooks" )
517+ require .NoError (t , os .MkdirAll (hooksDir , 0o755 ))
518+ hookPath := filepath .Join (hooksDir , "pre-merge-commit" )
519+ // Pre-existing hook with our marker but NO execute permissions.
520+ old := "#!/bin/sh\n " + preMergeCommitHookMarker + "\n # old\n "
521+ require .NoError (t , os .WriteFile (hookPath , []byte (old ), 0o644 ))
522+
523+ orig := executableFunc
524+ t .Cleanup (func () { executableFunc = orig })
525+ executableFunc = func () (string , error ) { return "/usr/local/bin/mdsmith" , nil }
526+
527+ require .NoError (t , ensurePreMergeCommitHook (dir , []string {"PLAN.md" }))
528+
529+ info , err := os .Stat (hookPath )
530+ require .NoError (t , err )
531+ // Verify execute bit is set despite the file existing without it.
532+ assert .NotZero (t , info .Mode ()& 0o111 ,
533+ "hook must have execute bit set even when overwriting non-executable file" )
534+ }
535+
511536func TestEnsurePreMergeCommitHook_RefusesUnmanagedHook (t * testing.T ) {
512537 dir := t .TempDir ()
513538 hooksDir := filepath .Join (dir , ".git" , "hooks" )
@@ -646,6 +671,26 @@ func TestEnsurePreMergeCommitHook_WriteFileFails(t *testing.T) {
646671 assert .Contains (t , err .Error (), "writing" )
647672}
648673
674+ func TestEnsurePreMergeCommitHook_ChmodFails (t * testing.T ) {
675+ dir := t .TempDir ()
676+ hooksDir := filepath .Join (dir , ".git" , "hooks" )
677+ require .NoError (t , os .MkdirAll (hooksDir , 0o755 ))
678+
679+ origExe := executableFunc
680+ t .Cleanup (func () { executableFunc = origExe })
681+ executableFunc = func () (string , error ) { return "/usr/local/bin/mdsmith" , nil }
682+
683+ origChmod := chmodFunc
684+ t .Cleanup (func () { chmodFunc = origChmod })
685+ chmodFunc = func (string , os.FileMode ) error {
686+ return os .ErrPermission
687+ }
688+
689+ err := ensurePreMergeCommitHook (dir , []string {"PLAN.md" })
690+ require .Error (t , err )
691+ assert .Contains (t , err .Error (), "setting permissions" )
692+ }
693+
649694// --- resolveHooksDir ---
650695
651696func TestResolveHooksDir_NotGitRepo (t * testing.T ) {
0 commit comments