diff --git a/internal/gitaly/service/hook/pre_receive.go b/internal/gitaly/service/hook/pre_receive.go
index 4062546dc2acc2d07b1543170a3a6240e58a453b..6beeb4da461c12550582f30e38e82d3b06aca2b2 100644
--- a/internal/gitaly/service/hook/pre_receive.go
+++ b/internal/gitaly/service/hook/pre_receive.go
@@ -7,10 +7,14 @@ import (
 	"os/exec"
 	"sync"
 
+	"github.com/golang/protobuf/ptypes/wrappers"
 	"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
+	"gitlab.com/gitlab-org/gitaly/v16/internal/helper/chunk"
 	"gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
 	"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
 	"gitlab.com/gitlab-org/gitaly/v16/streamio"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/wrapperspb"
 )
 
 func (s *server) PreReceiveHook(stream gitalypb.HookService_PreReceiveHookServer) error {
@@ -61,11 +65,56 @@ func validatePreReceiveHookRequest(ctx context.Context, locator storage.Locator,
 	return locator.ValidateRepository(ctx, in.GetRepository())
 }
 
+type preReceiveHookErrorSender struct {
+	errorCode int32
+	bytes     *wrappers.BytesValue
+	send      func(*wrappers.BytesValue, int32) error
+}
+
+func (p *preReceiveHookErrorSender) Reset() {
+	if p.bytes != nil {
+		p.bytes.Value = p.bytes.Value[:0]
+		return
+	}
+
+	p.bytes = &wrappers.BytesValue{}
+}
+
+func (p *preReceiveHookErrorSender) Append(m proto.Message) {
+	p.bytes.Value = append(p.bytes.Value, m.(*wrappers.BytesValue).Value...)
+}
+
+func (p *preReceiveHookErrorSender) Send() error {
+	return p.send(p.bytes, p.errorCode)
+}
+
 func preReceiveHookResponse(stream gitalypb.HookService_PreReceiveHookServer, code int32, stderr string) error {
-	if err := stream.Send(&gitalypb.PreReceiveHookResponse{
-		ExitStatus: &gitalypb.ExitStatus{Value: code},
-		Stderr:     []byte(stderr),
-	}); err != nil {
+	if stderr == "" {
+		if err := stream.Send(&gitalypb.PreReceiveHookResponse{
+			ExitStatus: &gitalypb.ExitStatus{Value: code},
+			Stderr:     []byte(""),
+		}); err != nil {
+			return structerr.NewInternal("sending response: %w", err)
+		}
+
+		return nil
+	}
+
+	chunker := chunk.New(&preReceiveHookErrorSender{
+		errorCode: code,
+		send: func(bytes *wrappers.BytesValue, errorCode int32) error {
+			return stream.Send(&gitalypb.PreReceiveHookResponse{
+				ExitStatus: &gitalypb.ExitStatus{Value: errorCode},
+				Stderr:     []byte(bytes.Value),
+			})
+		},
+	})
+
+	if err := chunker.Send(wrapperspb.Bytes([]byte(stderr))); err != nil {
+		return structerr.NewInternal("sending response: %w", err)
+	}
+
+	if err := chunker.Flush(); err != nil {
 		return structerr.NewInternal("sending response: %w", err)
 	}