diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
index 71fec803b4..db0efc7d8f 100644
--- a/NEXT_CHANGELOG.md
+++ b/NEXT_CHANGELOG.md
@@ -7,6 +7,7 @@
 ### Dependency updates
 
 ### CLI
+* Use OS aware runner instead of bash for run-local command ([#2996](https://github.com/databricks/cli/pull/2996))
 
 ### Bundles
 * Fix "bundle summary -o json" to render null values properly ([#2990](https://github.com/databricks/cli/pull/2990))
diff --git a/acceptance/cmd/workspace/apps/run-local/app/requirements.txt b/acceptance/cmd/workspace/apps/run-local/app/requirements.txt
new file mode 100644
index 0000000000..aa4b1291cf
--- /dev/null
+++ b/acceptance/cmd/workspace/apps/run-local/app/requirements.txt
@@ -0,0 +1 @@
+Flask==3.1.1
diff --git a/cmd/workspace/apps/run_local.go b/cmd/workspace/apps/run_local.go
index cfc4ac8fb5..adc200ef3c 100644
--- a/cmd/workspace/apps/run_local.go
+++ b/cmd/workspace/apps/run_local.go
@@ -53,7 +53,7 @@ func setupWorkspaceAndConfig(cmd *cobra.Command, entryPoint string, appPort int)
 func setupApp(cmd *cobra.Command, config *apps.Config, spec *apps.AppSpec, customEnv []string, prepareEnvironment bool) (apps.App, []string, error) {
 	ctx := cmd.Context()
 	cfg := cmdctx.ConfigUsed(ctx)
-	app := apps.NewApp(config, spec)
+	app := apps.NewApp(ctx, config, spec)
 	env := auth.ProcessEnv(cfg)
 	if cfg.Profile != "" {
 		env = append(env, "DATABRICKS_CONFIG_PROFILE="+cfg.Profile)
diff --git a/libs/apps/apps.go b/libs/apps/apps.go
index 7eeaf7f00c..420db61c52 100644
--- a/libs/apps/apps.go
+++ b/libs/apps/apps.go
@@ -1,12 +1,14 @@
 package apps
 
+import "context"
+
 type App interface {
 	PrepareEnvironment() error
 	GetCommand(bool) ([]string, error)
 }
 
-func NewApp(config *Config, spec *AppSpec) App {
+func NewApp(ctx context.Context, config *Config, spec *AppSpec) App {
 	// We only support python apps for now, but later we can add more types
 	// based on AppSpec
-	return NewPythonApp(config, spec)
+	return NewPythonApp(ctx, config, spec)
 }
diff --git a/libs/apps/python.go b/libs/apps/python.go
index 964b23e6b4..9360505909 100644
--- a/libs/apps/python.go
+++ b/libs/apps/python.go
@@ -1,13 +1,15 @@
 package apps
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strconv"
 	"strings"
+
+	"github.com/databricks/cli/libs/exec"
 )
 
 const DEBUG_PORT = "5678"
@@ -37,16 +39,17 @@ var defaultLibraries = []string{
 }
 
 type PythonApp struct {
+	ctx    context.Context
 	config *Config
 	spec   *AppSpec
 	uvArgs []string
 }
 
-func NewPythonApp(config *Config, spec *AppSpec) *PythonApp {
+func NewPythonApp(ctx context.Context, config *Config, spec *AppSpec) *PythonApp {
 	if config.DebugPort == "" {
 		config.DebugPort = DEBUG_PORT
 	}
-	return &PythonApp{config: config, spec: spec}
+	return &PythonApp{ctx: ctx, config: config, spec: spec}
 }
 
 // PrepareEnvironment creates a Python virtual environment using uv and installs required dependencies.
@@ -68,7 +71,9 @@ func (p *PythonApp) PrepareEnvironment() error {
 
 	// Install requirements if they exist
 	if _, err := os.Stat(filepath.Join(p.config.AppPath, "requirements.txt")); err == nil {
-		reqArgs := []string{"uv", "pip", "install", "-r", filepath.Join(p.config.AppPath, "requirements.txt")}
+		// We also execute command with CWD set at p.config.AppPath
+		// so we can just path local path to requirements.txt here
+		reqArgs := []string{"uv", "pip", "install", "-r", "requirements.txt"}
 		if err := p.runCommand(reqArgs); err != nil {
 			return err
 		}
@@ -133,11 +138,19 @@ func (p *PythonApp) enableDebugging() {
 	}
 }
 
-// runCommand executes the given command as a bash command and returns any error.
+// runCommand executes the given command and returns any error.
 func (p *PythonApp) runCommand(args []string) error {
-	cmd := exec.Command("bash", "-c", strings.Join(args, " "))
-	cmd.Dir = p.spec.config.AppPath
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	return cmd.Run()
+	e, err := exec.NewCommandExecutor(p.config.AppPath)
+	if err != nil {
+		return err
+	}
+	e.WithInheritOutput()
+
+	// Safe to join args with spaces here since args are passed directly inside PrepareEnvironment() and GetCommand()
+	// and don't contain user input.
+	cmd, err := e.StartCommand(p.ctx, strings.Join(args, " "))
+	if err != nil {
+		return err
+	}
+	return cmd.Wait()
 }
diff --git a/libs/apps/python_test.go b/libs/apps/python_test.go
index e2f9709ce9..dcbdc38aea 100644
--- a/libs/apps/python_test.go
+++ b/libs/apps/python_test.go
@@ -1,6 +1,7 @@
 package apps
 
 import (
+	"context"
 	"path/filepath"
 	"testing"
 
@@ -88,7 +89,7 @@ func TestPythonAppGetCommand(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			config, spec := tt.setup()
-			app := NewPythonApp(config, spec)
+			app := NewPythonApp(context.Background(), config, spec)
 			cmd, err := app.GetCommand(tt.debug)
 
 			if !tt.wantErr {
diff --git a/libs/exec/exec.go b/libs/exec/exec.go
index 466117e601..f3e777a423 100644
--- a/libs/exec/exec.go
+++ b/libs/exec/exec.go
@@ -61,21 +61,28 @@ func (c *command) Stderr() io.ReadCloser {
 }
 
 type Executor struct {
-	shell shell
-	dir   string
+	shell         shell
+	dir           string
+	inheritOutput bool
 }
 
+// NewCommandExecutor creates an Executor with default output behavior (no inheritance)
 func NewCommandExecutor(dir string) (*Executor, error) {
 	shell, err := findShell()
 	if err != nil {
 		return nil, err
 	}
 	return &Executor{
-		shell: shell,
-		dir:   dir,
+		shell:         shell,
+		dir:           dir,
+		inheritOutput: false,
 	}, nil
 }
 
+func (e *Executor) WithInheritOutput() {
+	e.inheritOutput = true
+}
+
 func NewCommandExecutorWithExecutable(dir string, execType ExecutableType) (*Executor, error) {
 	f, ok := finders[execType]
 	if !ok {
@@ -107,20 +114,24 @@ func (e *Executor) StartCommand(ctx context.Context, command string) (Command, e
 	if err != nil {
 		return nil, err
 	}
-	return e.start(ctx, cmd, ec)
+	return e.start(cmd, ec)
 }
 
-func (e *Executor) start(ctx context.Context, cmd *osexec.Cmd, ec *execContext) (Command, error) {
+func (e *Executor) start(cmd *osexec.Cmd, ec *execContext) (Command, error) {
+	if e.inheritOutput {
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		cmd.Stdin = os.Stdin
+		return &command{cmd, ec, nil, nil}, cmd.Start()
+	}
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		return nil, err
 	}
-
 	stderr, err := cmd.StderrPipe()
 	if err != nil {
 		return nil, err
 	}
-
 	return &command{cmd, ec, stdout, stderr}, cmd.Start()
 }
 
diff --git a/libs/exec/exec_test.go b/libs/exec/exec_test.go
index f245f9dd1e..49ab2eba66 100644
--- a/libs/exec/exec_test.go
+++ b/libs/exec/exec_test.go
@@ -123,7 +123,7 @@ func TestExecutorCleanupsTempFiles(t *testing.T) {
 	cmd, ec, err := executor.prepareCommand(context.Background(), "echo 'Hello'")
 	assert.NoError(t, err)
 
-	command, err := executor.start(context.Background(), cmd, ec)
+	command, err := executor.start(cmd, ec)
 	assert.NoError(t, err)
 
 	fileName := ec.args[1]
