Skip to content

Commit 7240b4f

Browse files
Aleksandr Slapoguzovslapoguzov
authored andcommitted
LLM-24245 strip shell prefix from commands in session updates and approval requests
1 parent 110a273 commit 7240b4f

4 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/CodexApprovalHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
} from "./app-server/v2";
1010
import type {ToolCallContent} from "@agentclientprotocol/sdk/dist/schema/types.gen";
1111
import {logger} from "./Logger";
12+
import {stripShellPrefix} from "./CodexEventHandler";
1213

1314
const APPROVAL_OPTIONS: acp.PermissionOption[] = [
1415
{ optionId: "allow_once", name: "Allow Once", kind: "allow_once" },
@@ -68,7 +69,7 @@ export class CodexApprovalHandler implements ApprovalHandler {
6869
kind: "execute",
6970
status: "pending",
7071
content: reasonContent ? [reasonContent] : null,
71-
rawInput: params.command ? { command: params.command, cwd: params.cwd } : null,
72+
rawInput: params.command ? { command: stripShellPrefix(params.command), cwd: params.cwd } : null,
7273
},
7374
options: APPROVAL_OPTIONS,
7475
};

src/CodexEventHandler.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ function toAcpStatus(status: CodexItemStatus): AcpToolCallStatus {
3737
}
3838
}
3939

40+
/**
41+
* Strips shell prefix from command string (e.g., "/bin/bash -lc 'command'", "/bin/zsh -c command")
42+
*/
43+
export function stripShellPrefix(command: string): string {
44+
const withoutShell = command.replace(/^(?:\/bin\/)?(?:bash|zsh|sh)\s+(?:-[lc]+\s+)?/, "");
45+
// Strip surrounding single quotes if present
46+
if (withoutShell.startsWith("'") && withoutShell.endsWith("'")) {
47+
return withoutShell.slice(1, -1);
48+
}
49+
return withoutShell;
50+
}
51+
4052
export class CodexEventHandler {
4153

4254
private readonly connection: acp.AgentSideConnection;
@@ -252,7 +264,7 @@ export class CodexEventHandler {
252264
if (commandAction) {
253265
return this.createCommandActionEvent(item.id, item.status, item.cwd, commandAction);
254266
}
255-
const command = item.command.replace(/^(?:\/bin\/)?bash\s+/, "");
267+
const command = stripShellPrefix(item.command);
256268
return {
257269
sessionUpdate: "tool_call",
258270
toolCallId: item.id,

src/__tests__/CodexACPAgent/approval-events.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,43 @@ describe('Approval Events', () => {
179179
completeTurn();
180180
await promptPromise;
181181
});
182+
183+
it.each([
184+
{ command: '/bin/zsh -c npm install', expected: 'npm install' },
185+
{ command: '/bin/bash -lc npm install', expected: 'npm install' },
186+
{ command: 'zsh npm install', expected: 'npm install' },
187+
{ command: 'sh -c ls -la', expected: 'ls -la' },
188+
{ command: 'npm install', expected: 'npm install' },
189+
{ command: "/bin/bash -lc './tests.cmd -Darg=value'", expected: './tests.cmd -Darg=value' },
190+
{ command: "/bin/zsh -c 'echo hello'", expected: 'echo hello' },
191+
])('should strip shell prefix from "$command" in rawInput', async ({ command, expected }) => {
192+
const { promptPromise, completeTurn } = setupSessionWithPendingPrompt();
193+
fixture.setPermissionResponse({
194+
outcome: { outcome: 'selected', optionId: 'allow_once' }
195+
});
196+
197+
const params: CommandExecutionRequestApprovalParams = {
198+
threadId: sessionId,
199+
turnId: 'turn-1',
200+
itemId: 'item-shell-prefix',
201+
reason: 'Installing dependencies',
202+
command,
203+
cwd: '/home/user/project',
204+
proposedExecpolicyAmendment: null,
205+
};
206+
207+
await fixture.sendServerRequest(
208+
'item/commandExecution/requestApproval',
209+
params
210+
);
211+
212+
const dump = fixture.getAcpConnectionDump(['_meta']);
213+
const parsed = JSON.parse(dump);
214+
expect(parsed.args[0].toolCall.rawInput.command).toBe(expected);
215+
216+
completeTurn();
217+
await promptPromise;
218+
});
182219
});
183220

184221
describe('File change approval', () => {

src/__tests__/CodexACPAgent/terminal-output-events.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,43 @@ describe('CodexEventHandler - terminal output events', () => {
7777
);
7878
});
7979

80+
it.each([
81+
{ command: '/bin/zsh -c npm install', expected: 'npm install' },
82+
{ command: '/bin/bash -lc npm install', expected: 'npm install' },
83+
{ command: 'zsh npm install', expected: 'npm install' },
84+
{ command: 'sh -c ls -la', expected: 'ls -la' },
85+
{ command: 'npm install', expected: 'npm install' },
86+
{ command: "/bin/bash -lc './tests.cmd -Darg=value'", expected: './tests.cmd -Darg=value' },
87+
{ command: "/bin/zsh -c 'echo hello'", expected: 'echo hello' },
88+
])('should strip shell prefix from "$command"', async ({ command, expected }) => {
89+
const commandStartNotification: ServerNotification = {
90+
method: 'item/started',
91+
params: {
92+
threadId: 'thread-1',
93+
turnId: 'turn-1',
94+
item: {
95+
type: 'commandExecution',
96+
id: 'command-shell-prefix',
97+
command,
98+
cwd: '/test/project',
99+
processId: null,
100+
status: 'inProgress',
101+
commandActions: [],
102+
aggregatedOutput: null,
103+
exitCode: null,
104+
durationMs: null,
105+
},
106+
},
107+
};
108+
109+
await setupAndSendNotifications([commandStartNotification]);
110+
111+
const dump = mockFixture.getAcpConnectionDump([]);
112+
const parsed = JSON.parse(dump);
113+
expect(parsed.args[0].update.title).toBe(expected);
114+
expect(parsed.args[0].update.rawInput.command).toBe(command);
115+
});
116+
80117
it('should stream terminal output delta', async () => {
81118
const outputDeltaNotification: ServerNotification = {
82119
method: 'item/commandExecution/outputDelta',

0 commit comments

Comments
 (0)