diff --git a/go.mod b/go.mod
index 526713514f99e71f0ea700ad04d73da5cb59c06b..b0563367ec5b85371a6fffd2545ab544d06f3416 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/vishvananda/netlink v1.3.0
 	github.com/vishvananda/netns v0.0.5
-	github.com/vladimirvivien/gexe v0.4.1
+	github.com/vladimirvivien/gexe v0.5.0
 	github.com/vmware/go-ipfix v0.13.0
 	golang.org/x/sys v0.31.0
 	google.golang.org/grpc v1.71.0
diff --git a/go.sum b/go.sum
index fb092baabb2f811aaf51c62f21c55d68bef21ce9..8c1104e2be5ed710986f2cab2cae7bf945e255a7 100644
--- a/go.sum
+++ b/go.sum
@@ -941,8 +941,8 @@ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
 github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
-github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY=
-github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E=
+github.com/vladimirvivien/gexe v0.5.0 h1:AWBVaYnrTsGYBktXvcO0DfWPeSiZxn6mnQ5nvL+A1/A=
+github.com/vladimirvivien/gexe v0.5.0/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E=
 github.com/vmware/go-ipfix v0.13.0 h1:v3paBzd7oq7LEU1SzDwD5RGoYcGROLQycYyN3EzLvDk=
 github.com/vmware/go-ipfix v0.13.0/go.mod h1:UTIR38AuEePzrWYjQOvnORCYRG33xZJ56E0K75mSosM=
 github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/builder.go b/vendor/github.com/vladimirvivien/gexe/exec/builder.go
index c5395c88835167b0d054c6dbd8b5953b7f96ba55..75e27ea78c689bbba84c455ed8d3c33aad2b2425 100644
--- a/vendor/github.com/vladimirvivien/gexe/exec/builder.go
+++ b/vendor/github.com/vladimirvivien/gexe/exec/builder.go
@@ -105,12 +105,14 @@ func (cr *PipedCommandResult) LastProc() *Proc {
 // CommandBuilder is a batch command builder that
 // can execute commands using different execution policies (i.e. serial, piped, concurrent)
 type CommandBuilder struct {
-	cmdPolicy CommandPolicy
-	procs     []*Proc
-	vars      *vars.Variables
-	err       error
-	stdout    io.Writer
-	stderr    io.Writer
+	cmdPolicy  CommandPolicy
+	procs      []*Proc
+	vars       *vars.Variables
+	err        error
+	stdout     io.Writer
+	stderr     io.Writer
+	shellStr   string
+	cmdStrings []string
 }
 
 // CommandsWithContextVars creates a *CommandBuilder with the specified context and session variables.
@@ -118,6 +120,7 @@ type CommandBuilder struct {
 func CommandsWithContextVars(ctx context.Context, variables *vars.Variables, cmds ...string) *CommandBuilder {
 	cb := new(CommandBuilder)
 	cb.vars = variables
+	cb.cmdStrings = cmds
 	for _, cmd := range cmds {
 		cb.procs = append(cb.procs, NewProcWithContextVars(ctx, cmd, variables))
 	}
@@ -175,6 +178,12 @@ func (cb *CommandBuilder) WithWorkDir(dir string) *CommandBuilder {
 	return cb
 }
 
+// WithShell sets the shell to use for all commands
+func (cb *CommandBuilder) WithShell(shell string) *CommandBuilder {
+	cb.shellStr = shell
+	return cb
+}
+
 // Run executes all commands successively and waits for all of the result. The result of each individual
 // command can be accessed from CommandResult.Procs[] after the execution completes. If policy == ExitOnErrPolicy, the
 // execution will stop on the first error encountered, otherwise it will continue. Processes with errors can be accessed
@@ -281,65 +290,6 @@ func (cb *CommandBuilder) Concurr() *CommandResult {
 	return cb.Start()
 }
 
-// Pipe executes each command serially chaining the combinedOutput of previous command to the inputPipe of next command.
-func (cb *CommandBuilder) Pipe() *PipedCommandResult {
-	if cb.err != nil {
-		return &PipedCommandResult{err: cb.err}
-	}
-
-	var result PipedCommandResult
-	procLen := len(cb.procs)
-	if procLen == 0 {
-		return &PipedCommandResult{}
-	}
-
-	// wire last proc to combined output
-	last := procLen - 1
-	result.lastProc = cb.procs[last]
-
-	// setup standard output/err for last proc in pipe
-	result.lastProc.cmd.Stdout = cb.stdout
-	if cb.stdout == nil {
-		result.lastProc.cmd.Stdout = result.lastProc.result
-	}
-
-	result.lastProc.cmd.Stderr = cb.stderr
-	if cb.stderr == nil {
-		result.lastProc.cmd.Stderr = result.lastProc.result
-	}
-
-	result.lastProc.cmd.Stdout = result.lastProc.result
-	for i, p := range cb.procs[:last] {
-		pipeout, err := p.cmd.StdoutPipe()
-		if err != nil {
-			p.err = err
-			return &PipedCommandResult{err: err, errProcs: []*Proc{p}}
-		}
-
-		cb.procs[i+1].cmd.Stdin = pipeout
-	}
-
-	// start each process (but, not wait for result)
-	// to ensure data flow between successive processes start
-	for _, p := range cb.procs {
-		result.procs = append(result.procs, p)
-		if err := p.Start().Err(); err != nil {
-			result.errProcs = append(result.errProcs, p)
-			return &result
-		}
-	}
-
-	// wait and access processes result
-	for _, p := range cb.procs {
-		if err := p.Wait().Err(); err != nil {
-			result.errProcs = append(result.errProcs, p)
-			break
-		}
-	}
-
-	return &result
-}
-
 func (cb *CommandBuilder) runCommand(proc *Proc) error {
 	// setup standard out and standard err
 
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/pipe_unix.go b/vendor/github.com/vladimirvivien/gexe/exec/pipe_unix.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b07a53bc0f0669ec4aee0de1389836dd6ccb4e4
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/exec/pipe_unix.go
@@ -0,0 +1,81 @@
+//go:build !windows
+
+package exec
+
+import "errors"
+
+// Pipe executes each command serially chaining the combinedOutput
+// of previous command to the input Pipe of next command.
+func (cb *CommandBuilder) Pipe() *PipedCommandResult {
+	if cb.err != nil {
+		return &PipedCommandResult{err: cb.err}
+	}
+
+	result := cb.connectProcPipes()
+
+	// check for structural errors
+	if result.err != nil {
+		return result
+	}
+
+	// start each process (but, not wait for result)
+	// to ensure data flow between successive processes start
+	for _, p := range cb.procs {
+		result.procs = append(result.procs, p)
+		if err := p.Start().Err(); err != nil {
+			result.errProcs = append(result.errProcs, p)
+			return result
+		}
+	}
+
+	// wait and access processes result
+	for _, p := range cb.procs {
+		if err := p.Wait().Err(); err != nil {
+			result.errProcs = append(result.errProcs, p)
+			break
+		}
+	}
+
+	return result
+}
+
+// connectProcPipes connects the output of each process to the input of the next process in the chain.
+// It returns a PipedCommandResult containing the connected processes and any errors encountered.
+func (cb *CommandBuilder) connectProcPipes() *PipedCommandResult {
+	var result PipedCommandResult
+
+	procLen := len(cb.procs)
+	if procLen == 0 {
+		return &PipedCommandResult{err: errors.New("no processes to connect")}
+	}
+
+	// wire last proc to combined output
+	last := procLen - 1
+	result.lastProc = cb.procs[last]
+
+	// setup standard output/err of last proc in pipe
+	result.lastProc.cmd.Stdout = cb.stdout
+	if cb.stdout == nil {
+		result.lastProc.cmd.Stdout = result.lastProc.result
+	}
+
+	// Wire standard error of last proc in pipe
+	result.lastProc.cmd.Stderr = cb.stderr
+	if cb.stderr == nil {
+		result.lastProc.cmd.Stderr = result.lastProc.result
+	}
+
+	// setup pipes for inner procs in the pipe chain
+	result.lastProc.cmd.Stdout = result.lastProc.result
+	for i, p := range cb.procs[:last] {
+		pipeout, err := p.cmd.StdoutPipe()
+		if err != nil {
+			p.err = err
+			return &PipedCommandResult{err: err, errProcs: []*Proc{p}}
+		}
+
+		cb.procs[i+1].cmd.Stdin = pipeout
+	}
+
+	return &result
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/pipe_windows.go b/vendor/github.com/vladimirvivien/gexe/exec/pipe_windows.go
new file mode 100644
index 0000000000000000000000000000000000000000..dde8ca1323fcbae3d3d8cae83e65b71e654580c9
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/exec/pipe_windows.go
@@ -0,0 +1,80 @@
+//go:build windows
+
+package exec
+
+import (
+	"bytes"
+	"errors"
+	"strings"
+)
+
+// Pipe executes each Windows command serially. Windows, however, does not support
+// OS pipes like {Li|U}nix. Instead, pipes use a single command string, with | delimiters,
+// passed to powershell. So prior to calling Pipe(), call CommandBulider.WithShell()
+// to specify "powershell.exe -c" as the shell.
+// (See tests for examples.)
+func (cb *CommandBuilder) Pipe() *PipedCommandResult {
+	if cb.err != nil {
+		return &PipedCommandResult{err: cb.err}
+	}
+
+	result := new(PipedCommandResult)
+
+	// setup a single command string with pipe delimiters
+	cmd := strings.Join(cb.cmdStrings, " | ")
+
+	// Prepend shell command if specified
+	if cb.shellStr != "" {
+		cmd = cb.shellStr + " " + cmd
+	}
+
+	proc := NewProcWithVars(cmd, cb.vars)
+	result.procs = append(result.procs, proc)
+	result.lastProc = proc
+
+	// execute the piped commands
+	if err := cb.runCommand(proc); err != nil {
+		return &PipedCommandResult{err: err, errProcs: []*Proc{proc}}
+	}
+
+	return result
+}
+
+// connectProcPipes connects the output of each process to the input of the next process in the chain.
+// It returns a PipedCommandResult containing the connected processes and any errors encountered.
+func (cb *CommandBuilder) connectProcPipes() *PipedCommandResult {
+	var result PipedCommandResult
+
+	procLen := len(cb.procs)
+	if procLen == 0 {
+		return &PipedCommandResult{err: errors.New("no processes to connect")}
+	}
+
+	// wire last proc to combined output
+	last := procLen - 1
+	result.lastProc = cb.procs[last]
+
+	// setup standard output/err for last proc in pipe
+	result.lastProc.cmd.Stdout = cb.stdout
+	if cb.stdout == nil {
+		result.lastProc.cmd.Stdout = result.lastProc.result
+	}
+
+	// Wire the remainder procs
+	result.lastProc.cmd.Stderr = cb.stderr
+	if cb.stderr == nil {
+		result.lastProc.cmd.Stderr = result.lastProc.result
+	}
+
+	// exec.Command.StdoutPipe() uses OS pipes, which are not supported on Windows.
+	// Instead, this uses an in-memory pipe and set the command's stdin to the write end of the pipe.
+	result.lastProc.cmd.Stdout = result.lastProc.result
+	for i := range cb.procs[:last] {
+		// Create an in-memory pipe for the command's stdout
+		pipe := new(bytes.Buffer)
+		cb.procs[i].cmd.Stdout = pipe
+		cb.procs[i+1].cmd.Stdin = pipe
+	}
+
+	return &result
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/proc.go b/vendor/github.com/vladimirvivien/gexe/exec/proc.go
index f9b6a520edd66dc04021506771b6ec0db1a1a372..65439ee48384d057c92a07543a284b6f3231cd58 100644
--- a/vendor/github.com/vladimirvivien/gexe/exec/proc.go
+++ b/vendor/github.com/vladimirvivien/gexe/exec/proc.go
@@ -10,7 +10,6 @@ import (
 	"os/user"
 	"strconv"
 	"strings"
-	"syscall"
 	"time"
 
 	"github.com/vladimirvivien/gexe/vars"
@@ -189,24 +188,7 @@ func (p *Proc) Start() *Proc {
 	}
 
 	// apply user id and user grp
-	var procCred *syscall.Credential
-	if p.userid != nil {
-		procCred = &syscall.Credential{
-			Uid: uint32(*p.userid),
-		}
-	}
-	if p.groupid != nil {
-		if procCred == nil {
-			procCred = new(syscall.Credential)
-		}
-		procCred.Uid = uint32(*p.groupid)
-	}
-	if procCred != nil {
-		if p.cmd.SysProcAttr == nil {
-			p.cmd.SysProcAttr = new(syscall.SysProcAttr)
-		}
-		p.cmd.SysProcAttr.Credential = procCred
-	}
+	p.applyCredentials()
 
 	if err := p.cmd.Start(); err != nil {
 		p.err = err
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/proc_unix.go b/vendor/github.com/vladimirvivien/gexe/exec/proc_unix.go
new file mode 100644
index 0000000000000000000000000000000000000000..cdea947a1b1acf900eaec2408969ae50b2d06ac5
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/exec/proc_unix.go
@@ -0,0 +1,30 @@
+//go:build !windows
+
+package exec
+
+import (
+	"syscall"
+)
+
+// applyCredentials applies the user and group IDs to the command.
+func (p *Proc) applyCredentials() {
+	// apply user id and user grp
+	var procCred *syscall.Credential
+	if p.userid != nil {
+		procCred = &syscall.Credential{
+			Uid: uint32(*p.userid),
+		}
+	}
+	if p.groupid != nil {
+		if procCred == nil {
+			procCred = new(syscall.Credential)
+		}
+		procCred.Gid = uint32(*p.groupid)
+	}
+	if procCred != nil {
+		if p.cmd.SysProcAttr == nil {
+			p.cmd.SysProcAttr = new(syscall.SysProcAttr)
+		}
+		p.cmd.SysProcAttr.Credential = procCred
+	}
+}
diff --git a/vendor/github.com/vladimirvivien/gexe/exec/proc_windows.go b/vendor/github.com/vladimirvivien/gexe/exec/proc_windows.go
new file mode 100644
index 0000000000000000000000000000000000000000..88ad91b9945fad839309f553f7d886c4c4e4c7bf
--- /dev/null
+++ b/vendor/github.com/vladimirvivien/gexe/exec/proc_windows.go
@@ -0,0 +1,9 @@
+//go:build windows
+
+package exec
+
+// applyCredentials is a no-op as this works vastly different on Windows.
+func (p *Proc) applyCredentials() {
+	// Windows doesn't support user/group IDs in the same way {Li|U}nix does.
+	// Windows impersonation will not be supported in this package a this time.
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 8ccdd81e7c795fafc96988efe87147be6d5759cb..ee9a7877ccb6371e9ea8084f3d34fe17fcf0c6e0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -549,7 +549,7 @@ github.com/vishvananda/netlink/nl
 # github.com/vishvananda/netns v0.0.5
 ## explicit; go 1.17
 github.com/vishvananda/netns
-# github.com/vladimirvivien/gexe v0.4.1
+# github.com/vladimirvivien/gexe v0.5.0
 ## explicit; go 1.23
 github.com/vladimirvivien/gexe
 github.com/vladimirvivien/gexe/exec