From 1fbde6efe2f0b94538fd55547668cba1884e8cc1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 4 Jun 2026 11:16:27 -0700 Subject: [PATCH] Avoid shell for system executables --- apps/server/src/diagnostics/ProcessDiagnostics.ts | 1 - .../environment/Layers/ServerEnvironmentLabel.ts | 1 - .../Layers/RepositoryIdentityResolver.test.ts | 1 - .../project/Layers/RepositoryIdentityResolver.ts | 2 -- apps/server/src/terminal/Layers/Manager.ts | 1 - packages/ssh/src/command.ts | 8 ++++---- packages/ssh/src/tunnel.ts | 6 +++--- packages/tailscale/src/tailscale.ts | 13 +++---------- 8 files changed, 10 insertions(+), 23 deletions(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index f56bf216513..b8ace8ccb39 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -280,7 +280,6 @@ const runProcess = Effect.fn("runProcess")( const child = yield* spawner.spawn( ChildProcess.make(input.command, input.args, { cwd: process.cwd(), - shell: process.platform === "win32", }), ); const [stdout, stderr, exitCode] = yield* Effect.all( diff --git a/apps/server/src/environment/Layers/ServerEnvironmentLabel.ts b/apps/server/src/environment/Layers/ServerEnvironmentLabel.ts index 7dc6bc34a43..b07425b936b 100644 --- a/apps/server/src/environment/Layers/ServerEnvironmentLabel.ts +++ b/apps/server/src/environment/Layers/ServerEnvironmentLabel.ts @@ -59,7 +59,6 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* ( command, args, timeoutBehavior: "timedOutResult", - shell: process.platform === "win32", }) .pipe(Effect.option); diff --git a/apps/server/src/project/Layers/RepositoryIdentityResolver.test.ts b/apps/server/src/project/Layers/RepositoryIdentityResolver.test.ts index a47181c8667..1c985cd8592 100644 --- a/apps/server/src/project/Layers/RepositoryIdentityResolver.test.ts +++ b/apps/server/src/project/Layers/RepositoryIdentityResolver.test.ts @@ -23,7 +23,6 @@ const git = (cwd: string, args: ReadonlyArray) => return yield* processRunner.run({ command: "git", args: ["-C", cwd, ...args], - shell: process.platform === "win32", }); }).pipe(Effect.provide(ProcessRunner.layer)); diff --git a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts index 926c1d0c2ec..4fdaa71de22 100644 --- a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts +++ b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts @@ -94,7 +94,6 @@ const resolveRepositoryIdentityCacheKey = Effect.fn("resolveRepositoryIdentityCa command: "git", args: ["-C", cwd, "rev-parse", "--show-toplevel"], timeoutBehavior: "timedOutResult", - shell: process.platform === "win32", }) .pipe(Effect.option); if (topLevelResult._tag === "None" || topLevelResult.value.code !== 0) { @@ -119,7 +118,6 @@ const resolveRepositoryIdentityFromCacheKey = Effect.fn("resolveRepositoryIdenti command: "git", args: ["-C", cacheKey, "remote", "-v"], timeoutBehavior: "timedOutResult", - shell: process.platform === "win32", }) .pipe(Effect.option); if (remoteResult._tag === "None" || remoteResult.value.code !== 0) { diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index 82eb9502cf6..2eaf19ec16c 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -519,7 +519,6 @@ function windowsInspectSubprocess( timeout: "1500 millis", maxOutputBytes: 32_768, outputMode: "truncate", - shell: process.platform === "win32", timeoutBehavior: "timedOutResult", }); }).pipe( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index 06aa1109edf..44cc047aae1 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -16,6 +16,7 @@ import { SshCommandError, SshInvalidTargetError } from "./errors.ts"; const PUBLISHABLE_T3_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u; const DEFAULT_SSH_COMMAND_TIMEOUT_MS = 60_000; const MAX_SSH_ERROR_OUTPUT_LENGTH = 4_000; +export const SSH_COMMAND = process.platform === "win32" ? "ssh.exe" : "ssh"; const encoder = new TextEncoder(); @@ -192,15 +193,14 @@ const runSshCommandInScope = Effect.fn("ssh/command.runSshCommand.inScope")(func const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; yield* Effect.logDebug("ssh.command.start", { ...sshTargetLogFields(target), - command: ["ssh", ...args], + command: [SSH_COMMAND, ...args], hasStdin: input.stdin !== undefined, timeoutMs: input.timeoutMs ?? DEFAULT_SSH_COMMAND_TIMEOUT_MS, }); const child = yield* spawner .spawn( - ChildProcess.make("ssh", args, { + ChildProcess.make(SSH_COMMAND, args, { env: environment, - shell: process.platform === "win32", stdin: { stream: stdinStream(input.stdin), endOnDone: true, @@ -212,7 +212,7 @@ const runSshCommandInScope = Effect.fn("ssh/command.runSshCommand.inScope")(func Effect.mapError( (cause) => new SshCommandError({ - command: ["ssh", ...args], + command: [SSH_COMMAND, ...args], exitCode: null, stderr: "", message: diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 88a30960e84..029b7644897 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -36,6 +36,7 @@ import { remoteStateKey, resolveSshTarget, runSshCommand, + SSH_COMMAND, targetConnectionKey, } from "./command.ts"; import { @@ -1068,7 +1069,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: `${input.localPort}:127.0.0.1:${input.remotePort}`, hostSpec, ]; - const tunnelCommand = ["ssh", ...args]; + const tunnelCommand = [SSH_COMMAND, ...args]; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const scope = yield* Scope.Scope; yield* Effect.logDebug("ssh.tunnel.spawn.start", { @@ -1081,9 +1082,8 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: }); const child = yield* spawner .spawn( - ChildProcess.make("ssh", args, { + ChildProcess.make(SSH_COMMAND, args, { env: childEnvironment, - shell: process.platform === "win32", stdin: { stream: Stream.empty, endOnDone: true, diff --git a/packages/tailscale/src/tailscale.ts b/packages/tailscale/src/tailscale.ts index c8d9cab462d..cdb549232b3 100644 --- a/packages/tailscale/src/tailscale.ts +++ b/packages/tailscale/src/tailscale.ts @@ -10,6 +10,7 @@ export const DEFAULT_TAILSCALE_SERVE_PORT = 443; export const TAILSCALE_STATUS_TIMEOUT_MS = 1_500; export const TAILSCALE_SERVE_TIMEOUT_MS = 10_000; export const TAILSCALE_PROBE_TIMEOUT_MS = 2_500; +const TAILSCALE_COMMAND = process.platform === "win32" ? "tailscale.exe" : "tailscale"; export class TailscaleCommandError extends Data.TaggedError("TailscaleCommandError")<{ readonly command: readonly string[]; @@ -136,11 +137,7 @@ export const readTailscaleStatus: Effect.Effect< const args = ["status", "--json"]; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* spawner - .spawn( - ChildProcess.make("tailscale", args, { - shell: process.platform === "win32", - }), - ) + .spawn(ChildProcess.make(TAILSCALE_COMMAND, args)) .pipe( Effect.mapError((cause) => tailscaleCommandError( @@ -215,11 +212,7 @@ const runTailscaleCommand = ( Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* spawner - .spawn( - ChildProcess.make("tailscale", args, { - shell: process.platform === "win32", - }), - ) + .spawn(ChildProcess.make(TAILSCALE_COMMAND, args)) .pipe( Effect.mapError((cause) => tailscaleCommandError(