diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/shell/command.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/shell/command.rb
index d7f727f92edd2f277a97f56ad289ded94fcd4b47..9089a5d480499f97ab04c9ff3090655aafe87996 100644
--- a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/shell/command.rb
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/shell/command.rb
@@ -17,6 +17,13 @@ class Command
           # @attr [Float] duration
           Result = Struct.new(:stdout, :stderr, :status, :duration, keyword_init: true)
 
+          # Result data structure from running a command in single pipeline mode
+          #
+          # @attr [String] stderr
+          # @attr [Process::Status] status
+          # @attr [Float] duration
+          SinglePipelineResult = Struct.new(:stderr, :status, :duration, keyword_init: true)
+
           # @example Usage
           #   Shell.new('echo', 'Some amazing output').capture
           # @param [Array<String>] cmd_args
@@ -36,6 +43,33 @@ def capture
 
             Result.new(stdout: stdout, stderr: stderr, status: status, duration: duration)
           end
+
+          # Run single command in pipeline mode with optional input or output redirection
+          #
+          # @param [IO|String|Array] input stdin redirection
+          # @param [IO|String|Array] output stdout redirection
+          # @return [Command::SinglePipelineResult]
+          def run_single_pipeline!(input: nil, output: nil)
+            start = Time.now
+            # Open3 writes on `err_write` and we receive from `err_read`
+            err_read, err_write = IO.pipe
+
+            # Pipeline accepts custom {Process.spawn} options
+            # stderr capture is always performed, stdin and stdout redirection
+            # are performed only when either `input` or `output` are present
+            options = { err: err_write } # redirect stderr to IO pipe
+            options[:in] = input if input # redirect stdin
+            options[:out] = output if output # redirect stdout
+
+            status_list = Open3.pipeline(cmd_args, **options)
+            duration = Time.now - start
+
+            err_write.close # close the pipe before reading
+            stderr = err_read.read
+            err_read.close # close after reading to avoid leaking file descriptors
+
+            SinglePipelineResult.new(stderr: stderr, status: status_list[0], duration: duration)
+          end
         end
       end
     end