diff --git a/CHANGELOG.md b/CHANGELOG.md index ad08d0330..c121279ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.clawd.bot - Web search: infer Perplexity base URL from API key source (direct vs OpenRouter). - TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs. - CLI: avoid duplicating --profile/--dev flags when formatting commands. +- Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297) — thanks @ysqander. ## 2026.1.19-3 diff --git a/README.md b/README.md index b8e0be195..e00703956 100644 --- a/README.md +++ b/README.md @@ -475,21 +475,22 @@ Thanks to all clawtributors: claude jamesgroat Hyaxia dantelex daveonkels mteam88 Eng. Juan Combetto dbhurley Mariano Belinky TSavo julianengel benithors timolins nachx639 sreekaransrinath gupsammy cristip73 nachoiacovino Vasanth Rao Naik Sabavat cpojer lc0rp scald gumadeiras andranik-sahakyan davidguttman sleontenko sircrumpet peschee rafaelreis-r thewilloftheshadow - ratulsarna lutr0 danielz1z bradleypriest emanuelst KristijanJovanovski CashWilliams rdev osolmaz joshrad-dev + ratulsarna lutr0 bradleypriest danielz1z emanuelst KristijanJovanovski CashWilliams rdev osolmaz joshrad-dev kiranjd adityashaw2 sheeek artuskg onutc manuelhettich minghinmatthewlam myfunc buddyh connorshea mcinteerj timkrase zerone0x gerardward2007 obviyus tosh-hamburg azade-c roshanasingh4 bjesuiter cheeeee - Josh Phillips YuriNachos chriseidhof tyler6204 superman32432432 vignesh07 Yurii Chukhlib antons austinm911 blacksmith-sh[bot] - dan-dr grp06 HeimdallStrategy imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 petter-b pkrmf - RandyVentures Ryan Lisse erikpr1994 Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot neist - ngutman chrisrodz Friederike Seiler gabriel-trigo iamadig Kit koala73 manmal ogulcancelik pasogott - petradonka rubyrunsstuff VACInc wes-davis zats Chris Taylor Django Navarro evalexpr henrino3 larlyssa - mkbehr oswalpalash pcty-nextgen-service-account sibbl Syhids Aaron Konyer aaronveklabs adam91holt dougvk erik-agens - fcatuhe ivanrvpereira jayhickey jeffersonwarrior jeffersonwarrior Jonathan D. Rhyne (DJ-D) jverdi longmaba mickahouan mjrussell - p6l-richard philipp-spiess robaxelsen Sash Catanzarite T5-AndyML VAC zknicker alejandro maza andrewting19 anpoirier - Asleep123 bolismauro cash-echo-bot Clawd conhecendocontato Dimitrios Ploutarchos Drake Thomsen Felix Krause gtsifrikas HazAT - hrdwdmrbl hugobarauna Jamie Openshaw Jarvis Jefferson Nunn Kevin Lin kitze levifig Lloyd loukotal - martinpucik Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 prathamdby reeltimeapps RLTCmpe - rodrigouroz Rolf Fredheim Rony Kelner Samrat Jha siraht snopoke suminhthanh The Admiral thesash Ubuntu - voidserf wstock Zach Knickerbocker Alphonse-arianee Azade carlulsoe ddyo Erik latitudeki5223 Manuel Maly - Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock + Josh Phillips YuriNachos chriseidhof tyler6204 ysqander superman32432432 vignesh07 Yurii Chukhlib grp06 antons + austinm911 blacksmith-sh[bot] dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 petter-b + pkrmf RandyVentures Ryan Lisse erikpr1994 Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot + neist ngutman chrisrodz Friederike Seiler gabriel-trigo iamadig Kit koala73 manmal ogulcancelik + pasogott petradonka rubyrunsstuff VACInc wes-davis zats 24601 Chris Taylor Django Navarro evalexpr + henrino3 humanwritten larlyssa mkbehr oswalpalash pcty-nextgen-service-account sibbl Syhids Aaron Konyer aaronveklabs + adam91holt dougvk erik-agens fcatuhe ivanrvpereira jayhickey jeffersonwarrior jeffersonwarrior Jonathan D. Rhyne (DJ-D) jverdi + longmaba mickahouan mjrussell p6l-richard philipp-spiess robaxelsen Sash Catanzarite T5-AndyML VAC zknicker + alejandro maza andrewting19 anpoirier Asleep123 bolismauro cash-echo-bot Clawd conhecendocontato Dimitrios Ploutarchos Drake Thomsen + Felix Krause gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jarvis Jefferson Nunn Kevin Lin kitze + levifig Lloyd loukotal martinpucik Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 + prathamdby reeltimeapps RLTCmpe rodrigouroz Rolf Fredheim Rony Kelner Samrat Jha siraht snopoke suminhthanh + The Admiral thesash Ubuntu voidserf wstock Zach Knickerbocker Alphonse-arianee Azade carlulsoe ddyo + Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani + William Stock

diff --git a/docs/tools/exec.md b/docs/tools/exec.md index 1a65618e2..3cf2ada1b 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -32,6 +32,8 @@ Notes: - `gateway`/`node` approvals are controlled by `~/.clawdbot/exec-approvals.json`. - `node` requires a paired node (companion app or headless node host). - If multiple nodes are available, set `exec.node` or `tools.exec.node` to select one. +- On non-Windows hosts, exec uses `SHELL` when set; if `SHELL` is `fish`, it prefers `bash` (or `sh`) + from `PATH` to avoid fish-incompatible scripts, then falls back to `SHELL` if neither exists. ## Config diff --git a/scripts/clawtributors-map.json b/scripts/clawtributors-map.json index d6ce6e27e..7ad1f926c 100644 --- a/scripts/clawtributors-map.json +++ b/scripts/clawtributors-map.json @@ -10,7 +10,8 @@ "longmaba", "manmal", "thesash", - "rhjoh" + "rhjoh", + "ysqander" ], "seedCommit": "d6863f87", "placeholderAvatar": "assets/avatar-placeholder.svg", diff --git a/src/agents/shell-utils.test.ts b/src/agents/shell-utils.test.ts new file mode 100644 index 000000000..5f781b001 --- /dev/null +++ b/src/agents/shell-utils.test.ts @@ -0,0 +1,82 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { getShellConfig } from "./shell-utils.js"; + +const isWin = process.platform === "win32"; + +describe("getShellConfig", () => { + const originalShell = process.env.SHELL; + const originalPath = process.env.PATH; + const tempDirs: string[] = []; + + const createTempBin = (files: string[]) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-shell-")); + tempDirs.push(dir); + for (const name of files) { + const filePath = path.join(dir, name); + fs.writeFileSync(filePath, ""); + fs.chmodSync(filePath, 0o755); + } + return dir; + }; + + beforeEach(() => { + if (!isWin) { + process.env.SHELL = "/usr/bin/fish"; + } + }); + + afterEach(() => { + if (originalShell == null) { + delete process.env.SHELL; + } else { + process.env.SHELL = originalShell; + } + if (originalPath == null) { + delete process.env.PATH; + } else { + process.env.PATH = originalPath; + } + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + if (isWin) { + it("uses PowerShell on Windows", () => { + const { shell } = getShellConfig(); + expect(shell.toLowerCase()).toContain("powershell"); + }); + return; + } + + it("prefers bash when fish is default and bash is on PATH", () => { + const binDir = createTempBin(["bash"]); + process.env.PATH = binDir; + const { shell } = getShellConfig(); + expect(shell).toBe(path.join(binDir, "bash")); + }); + + it("falls back to sh when fish is default and bash is missing", () => { + const binDir = createTempBin(["sh"]); + process.env.PATH = binDir; + const { shell } = getShellConfig(); + expect(shell).toBe(path.join(binDir, "sh")); + }); + + it("falls back to env shell when fish is default and no sh is available", () => { + process.env.PATH = ""; + const { shell } = getShellConfig(); + expect(shell).toBe("/usr/bin/fish"); + }); + + it("uses sh when SHELL is unset", () => { + delete process.env.SHELL; + process.env.PATH = ""; + const { shell } = getShellConfig(); + expect(shell).toBe("sh"); + }); +}); diff --git a/src/agents/shell-utils.ts b/src/agents/shell-utils.ts index bad3e7d80..6974d9a2a 100644 --- a/src/agents/shell-utils.ts +++ b/src/agents/shell-utils.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; import { spawn } from "node:child_process"; -import path from "node:path"; function resolvePowerShellPath(): string { const systemRoot = process.env.SystemRoot || process.env.WINDIR; @@ -35,12 +34,31 @@ export function getShellConfig(): { shell: string; args: string[] } { const shellName = envShell ? path.basename(envShell) : ""; // Fish rejects common bashisms used by tools, so prefer bash when detected. if (shellName === "fish") { - return { shell: "/bin/bash", args: ["-c"] }; + const bash = resolveShellFromPath("bash"); + if (bash) return { shell: bash, args: ["-c"] }; + const sh = resolveShellFromPath("sh"); + if (sh) return { shell: sh, args: ["-c"] }; } - const shell = envShell || "sh"; + const shell = envShell && envShell.length > 0 ? envShell : "sh"; return { shell, args: ["-c"] }; } +function resolveShellFromPath(name: string): string | undefined { + const envPath = process.env.PATH ?? ""; + if (!envPath) return undefined; + const entries = envPath.split(path.delimiter).filter(Boolean); + for (const entry of entries) { + const candidate = path.join(entry, name); + try { + fs.accessSync(candidate, fs.constants.X_OK); + return candidate; + } catch { + // ignore missing or non-executable entries + } + } + return undefined; +} + export function sanitizeBinaryOutput(text: string): string { const scrubbed = text.replace(/[\p{Format}\p{Surrogate}]/gu, ""); if (!scrubbed) return scrubbed; diff --git a/src/process/command-queue.ts b/src/process/command-queue.ts index 51b21881c..8e4e7377d 100644 --- a/src/process/command-queue.ts +++ b/src/process/command-queue.ts @@ -111,7 +111,7 @@ export function enqueueCommand( return enqueueCommandInLane(CommandLane.Main, task, opts); } -export function getQueueSize(lane = CommandLane.Main) { +export function getQueueSize(lane: string = CommandLane.Main) { const state = lanes.get(lane); if (!state) return 0; return state.queue.length + state.active; @@ -125,7 +125,7 @@ export function getTotalQueueSize() { return total; } -export function clearCommandLane(lane = CommandLane.Main) { +export function clearCommandLane(lane: string = CommandLane.Main) { const cleaned = lane.trim() || CommandLane.Main; const state = lanes.get(cleaned); if (!state) return 0;