Skip to content

Commit 75c2d13

Browse files
committed
feat(bash): add env parameter for setting environment variables
Add optional `env` parameter to the bash tool that allows passing environment variables to the spawned process. This enables plugins to inject environment variables like AGENT_MODE and AGENT_ID to track whether commands are running in primary or subagent sessions. Changes: - Add `env` parameter to bash tool schema (z.record(z.string(), z.string())) - Merge env vars with process.env in spawn call - Add tests for single and multiple environment variables
1 parent 74b14a2 commit 75c2d13

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

packages/opencode/src/tool/bash.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const Parameters = z.object({
6363
.describe(
6464
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
6565
),
66+
env: z.record(z.string(), z.string()).optional().describe("Environment variables to set for the command"),
6667
})
6768

6869
type Part = {
@@ -243,9 +244,14 @@ const ask = Effect.fn("BashTool.ask")(function* (ctx: Tool.Context, scan: Scan)
243244
})
244245
})
245246

247+
function pwshEncodedCommand(command: string) {
248+
return Buffer.from(command, "utf16le").toString("base64")
249+
}
250+
246251
function cmd(shell: string, name: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
247252
if (process.platform === "win32" && PS.has(name)) {
248-
return ChildProcess.make(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], {
253+
const encoded = pwshEncodedCommand(command)
254+
return ChildProcess.make(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
249255
cwd,
250256
env,
251257
stdin: "ignore",
@@ -493,7 +499,7 @@ export const BashTool = Tool.define(
493499
name,
494500
command: params.command,
495501
cwd,
496-
env: yield* shellEnv(ctx, cwd),
502+
env: { ...(yield* shellEnv(ctx, cwd)), ...(params.env || {}) },
497503
timeout,
498504
description: params.description,
499505
},

packages/opencode/src/tool/bash.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Before executing the command, please follow these steps:
2525
Usage notes:
2626
- The command argument is required.
2727
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
28+
- You can specify an optional env parameter to set environment variables for the command (e.g., `env: { "MY_VAR": "value" }`). These are merged with the existing environment.
2829
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
2930
- If the output exceeds ${maxLines} lines or ${maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use `head`, `tail`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
3031

packages/opencode/test/tool/bash.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const shells = (() => {
6464
(item, i) => list.findIndex((other) => other.shell.toLowerCase() === item.shell.toLowerCase()) === i,
6565
)
6666
})()
67+
6768
const PS = new Set(["pwsh", "powershell"])
6869
const ps = shells.filter((item) => PS.has(item.label))
6970

@@ -1192,3 +1193,54 @@ describe("tool.bash truncation", () => {
11921193
})
11931194
})
11941195
})
1196+
1197+
describe("tool.bash env", () => {
1198+
each("sets environment variables", async () => {
1199+
await Instance.provide({
1200+
directory: projectRoot,
1201+
fn: async () => {
1202+
const bash = await initBash()
1203+
const expr = `process.env.TEST_VAR`
1204+
const cmd = `${bin} -p ${evalarg(expr)}`
1205+
const command = PS.has(sh()) ? `& ${cmd}` : cmd
1206+
const result = await Effect.runPromise(
1207+
bash.execute(
1208+
{
1209+
command,
1210+
description: "Echo environment variable",
1211+
env: { TEST_VAR: "hello_world" },
1212+
},
1213+
ctx,
1214+
),
1215+
)
1216+
expect(result.metadata.exit).toBe(0)
1217+
expect(result.output).toContain("hello_world")
1218+
},
1219+
})
1220+
})
1221+
1222+
each("sets multiple environment variables", async () => {
1223+
await Instance.provide({
1224+
directory: projectRoot,
1225+
fn: async () => {
1226+
const bash = await initBash()
1227+
const expr = `process.env.VAR1 + " " + process.env.VAR2`
1228+
const cmd = `${bin} -p ${evalarg(expr)}`
1229+
const command = PS.has(sh()) ? `& ${cmd}` : cmd
1230+
const result = await Effect.runPromise(
1231+
bash.execute(
1232+
{
1233+
command,
1234+
description: "Echo multiple environment variables",
1235+
env: { VAR1: "foo", VAR2: "bar" },
1236+
},
1237+
ctx,
1238+
),
1239+
)
1240+
expect(result.metadata.exit).toBe(0)
1241+
expect(result.output).toContain("foo")
1242+
expect(result.output).toContain("bar")
1243+
},
1244+
})
1245+
})
1246+
})

0 commit comments

Comments
 (0)