Skip to content

Commit f61e1a5

Browse files
committed
fix(non-interactive-env): use export for env vars to apply to all chained commands
Previous `VAR=val cmd` format only applied to first command in chains. New `export VAR=val; cmd` format ensures variables persist for all commands. Also increased test timeouts for todo-continuation-enforcer stability. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
1 parent 03c51c9 commit f61e1a5

File tree

3 files changed

+37
-16
lines changed

3 files changed

+37
-16
lines changed

src/hooks/non-interactive-env/index.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe("non-interactive-env hook", () => {
55
const mockCtx = {} as Parameters<typeof createNonInteractiveEnvHook>[0]
66

77
describe("git command modification", () => {
8-
test("#given git command #when hook executes #then prepends env vars", async () => {
8+
test("#given git command #when hook executes #then prepends export statement", async () => {
99
const hook = createNonInteractiveEnvHook(mockCtx)
1010
const output: { args: Record<string, unknown>; message?: string } = {
1111
args: { command: "git commit -m 'test'" },
@@ -17,10 +17,27 @@ describe("non-interactive-env hook", () => {
1717
)
1818

1919
const cmd = output.args.command as string
20+
expect(cmd).toStartWith("export ")
2021
expect(cmd).toContain("GIT_EDITOR=:")
2122
expect(cmd).toContain("EDITOR=:")
2223
expect(cmd).toContain("PAGER=cat")
23-
expect(cmd).toEndWith(" git commit -m 'test'")
24+
expect(cmd).toContain("; git commit -m 'test'")
25+
})
26+
27+
test("#given chained git commands #when hook executes #then export applies to all", async () => {
28+
const hook = createNonInteractiveEnvHook(mockCtx)
29+
const output: { args: Record<string, unknown>; message?: string } = {
30+
args: { command: "git add file && git rebase --continue" },
31+
}
32+
33+
await hook["tool.execute.before"](
34+
{ tool: "bash", sessionID: "test", callID: "1" },
35+
output
36+
)
37+
38+
const cmd = output.args.command as string
39+
expect(cmd).toStartWith("export ")
40+
expect(cmd).toContain("; git add file && git rebase --continue")
2441
})
2542

2643
test("#given non-git bash command #when hook executes #then command unchanged", async () => {

src/hooks/non-interactive-env/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,18 @@ function shellEscape(value: string): string {
3434
}
3535

3636
/**
37-
* Build env prefix string with line continuation for readability:
38-
* VAR1=val1 \
39-
* VAR2=val2 \
40-
* ...
37+
* Build export statement for environment variables.
38+
* Uses `export VAR1=val1 VAR2=val2;` format to ensure variables
39+
* apply to ALL commands in a chain (e.g., `cmd1 && cmd2`).
40+
*
41+
* Previous approach used VAR=value prefix which only applies to the first command.
4142
* OpenCode's bash tool ignores args.env, so we must prepend to command.
4243
*/
4344
function buildEnvPrefix(env: Record<string, string>): string {
44-
return Object.entries(env)
45+
const exports = Object.entries(env)
4546
.map(([key, value]) => `${key}=${shellEscape(value)}`)
46-
.join(" \\\n")
47+
.join(" ")
48+
return `export ${exports};`
4749
}
4850

4951
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
@@ -73,9 +75,11 @@ export function createNonInteractiveEnvHook(_ctx: PluginInput) {
7375
}
7476

7577
// OpenCode's bash tool uses hardcoded `...process.env` in spawn(),
76-
// ignoring any args.env we might set. Prepend to command instead.
78+
// ignoring any args.env we might set. Prepend export statement to command.
79+
// Uses `export VAR=val;` format to ensure variables apply to ALL commands
80+
// in a chain (e.g., `git add file && git rebase --continue`).
7781
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV)
78-
output.args.command = `${envPrefix} \\\n${command}`
82+
output.args.command = `${envPrefix} ${command}`
7983

8084
log(`[${HOOK_NAME}] Prepended non-interactive env vars to git command`, {
8185
sessionID: input.sessionID,

src/hooks/todo-continuation-enforcer.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ describe("todo-continuation-enforcer", () => {
415415
await hook.handler({
416416
event: { type: "session.idle", properties: { sessionID } },
417417
})
418-
await new Promise(r => setTimeout(r, 2500))
418+
await new Promise(r => setTimeout(r, 3500))
419419

420420
// #then - first injection happened
421421
expect(promptCalls.length).toBe(1)
@@ -424,11 +424,11 @@ describe("todo-continuation-enforcer", () => {
424424
await hook.handler({
425425
event: { type: "session.idle", properties: { sessionID } },
426426
})
427-
await new Promise(r => setTimeout(r, 2500))
427+
await new Promise(r => setTimeout(r, 3500))
428428

429429
// #then - second injection also happened (no throttle blocking)
430430
expect(promptCalls.length).toBe(2)
431-
}, { timeout: 10000 })
431+
}, { timeout: 15000 })
432432

433433
// ============================================================
434434
// ABORT "IMMEDIATELY BEFORE" DETECTION TESTS
@@ -589,19 +589,19 @@ describe("todo-continuation-enforcer", () => {
589589
event: { type: "session.idle", properties: { sessionID } },
590590
})
591591

592-
await new Promise(r => setTimeout(r, 3000))
592+
await new Promise(r => setTimeout(r, 3500))
593593
expect(promptCalls).toHaveLength(0)
594594

595595
// #when - second idle event occurs (abort is no longer "immediately before")
596596
await hook.handler({
597597
event: { type: "session.idle", properties: { sessionID } },
598598
})
599599

600-
await new Promise(r => setTimeout(r, 2500))
600+
await new Promise(r => setTimeout(r, 3500))
601601

602602
// #then - continuation injected on second idle (abort state was consumed)
603603
expect(promptCalls.length).toBe(1)
604-
}, { timeout: 10000 })
604+
}, { timeout: 15000 })
605605

606606
test("should handle multiple abort errors correctly - only last one matters", async () => {
607607
// #given - session with incomplete todos

0 commit comments

Comments
 (0)