Skip to content

Commit d6914bd

Browse files
fix(core): align shell tool description with configured shell (#4170)
1 parent b90a2c9 commit d6914bd

3 files changed

Lines changed: 248 additions & 47 deletions

File tree

packages/core/src/tools/__snapshots__/shell.test.ts.snap

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`ShellTool > getDescription > should return the non-windows description when not on windows 1`] = `
4-
"Executes a given shell command (as \`bash -c <command>\`) in a persistent shell session with optional timeout, ensuring proper handling and security measures.
4+
"Executes a given shell command (as \`bash -c <command>\`) in a subprocess with optional timeout, ensuring proper handling and security measures.
55
66
IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
77
@@ -17,7 +17,7 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
1717
- Edit files: Use edit (NOT sed/awk)
1818
- Write files: Use write_file (NOT echo >/cat <<EOF)
1919
- Communication: Output text directly (NOT echo/printf)
20-
- **Shell argument quoting and special characters**: When passing arguments that contain special characters (parentheses \`()\`, backticks \`\`\`\`, dollar signs \`$\`, backslashes \`\\\`, semicolons \`;\`, pipes \`|\`, angle brackets \`<>\`, ampersands \`&\`, exclamation marks \`!\`, etc.), you MUST ensure they are properly quoted to prevent the shell from misinterpreting them as shell syntax:
20+
- **Shell argument quoting and special characters**: The active shell is Bash. When passing arguments that contain special characters (parentheses \`()\`, backticks \`\`\`\`, dollar signs \`$\`, backslashes \`\\\`, semicolons \`;\`, pipes \`|\`, angle brackets \`<>\`, ampersands \`&\`, exclamation marks \`!\`, etc.), you MUST ensure they are properly quoted to prevent Bash from misinterpreting them as shell syntax:
2121
- **Single quotes** \`'...'\` pass everything literally, but cannot contain a literal single quote.
2222
- **ANSI-C quoting** \`$'...'\` supports escape sequences (e.g. \`\\n\` for newline, \`\\'\` for single quote) and is the safest approach for multi-line strings or strings with single quotes.
2323
- **Heredoc** is the most robust approach for large, multi-line text with mixed quotes:
@@ -32,8 +32,8 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
3232
- When issuing multiple commands:
3333
- If the commands are independent and can run in parallel, make multiple run_shell_command tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two run_shell_command tool calls in parallel.
3434
- If the commands depend on each other and must run sequentially, use a single run_shell_command call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before run_shell_command for git operations, or git add before git commit), run these operations sequentially instead.
35-
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
36-
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
35+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
36+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings).
3737
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
3838
<good-example>
3939
pytest /foo/bar/tests
@@ -62,7 +62,7 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
6262
`;
6363
6464
exports[`ShellTool > getDescription > should return the windows description when on windows 1`] = `
65-
"Executes a given shell command (as \`cmd.exe /c <command>\`) in a persistent shell session with optional timeout, ensuring proper handling and security measures.
65+
"Executes a given shell command (as \`cmd.exe /d /s /c <command>\`) in a subprocess with optional timeout, ensuring proper handling and security measures.
6666
6767
IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
6868
@@ -78,23 +78,17 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
7878
- Edit files: Use edit (NOT sed/awk)
7979
- Write files: Use write_file (NOT echo >/cat <<EOF)
8080
- Communication: Output text directly (NOT echo/printf)
81-
- **Shell argument quoting and special characters**: When passing arguments that contain special characters (parentheses \`()\`, backticks \`\`\`\`, dollar signs \`$\`, backslashes \`\\\`, semicolons \`;\`, pipes \`|\`, angle brackets \`<>\`, ampersands \`&\`, exclamation marks \`!\`, etc.), you MUST ensure they are properly quoted to prevent the shell from misinterpreting them as shell syntax:
82-
- **Single quotes** \`'...'\` pass everything literally, but cannot contain a literal single quote.
83-
- **ANSI-C quoting** \`$'...'\` supports escape sequences (e.g. \`\\n\` for newline, \`\\'\` for single quote) and is the safest approach for multi-line strings or strings with single quotes.
84-
- **Heredoc** is the most robust approach for large, multi-line text with mixed quotes:
85-
\`\`\`bash
86-
gh pr create --title "My Title" --body "$(cat <<'HEREDOC'
87-
Multi-line body with (parentheses), \`backticks\`, and 'single-quotes'.
88-
HEREDOC
89-
)"
90-
\`\`\`
91-
- NEVER use unescaped single quotes inside single-quoted strings (e.g. \`'it\\'s'\` is wrong; use \`$'it\\'s'\` or \`"it's"\` instead).
92-
- If unsure, prefer double-quoting arguments and escape inner double-quotes as \`\\"\`.
81+
- **Shell argument quoting and special characters**: The active shell is cmd.exe. When passing arguments that contain special characters (parentheses \`()\`, backticks \`\`\`\`, dollar signs \`$\`, backslashes \`\\\`, semicolons \`;\`, pipes \`|\`, angle brackets \`<>\`, ampersands \`&\`, exclamation marks \`!\`, etc.), you MUST ensure they are properly quoted to prevent cmd.exe from misinterpreting them as shell syntax:
82+
- Use double quotes around arguments that contain spaces or metacharacters.
83+
- Escape literal cmd.exe metacharacters such as \`&\`, \`|\`, \`<\`, \`>\`, and \`^\` with caret (\`^\`).
84+
- Single quotes do not quote arguments in cmd.exe.
85+
- Be careful with \`%VAR%\` environment-variable expansion; avoid literal \`%...%\` unless expansion is intended.
86+
- Do NOT use Bash-only forms such as ANSI-C quoting (\`$'...'\`) or Bash heredocs.
9387
- When issuing multiple commands:
9488
- If the commands are independent and can run in parallel, make multiple run_shell_command tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two run_shell_command tool calls in parallel.
95-
- If the commands depend on each other and must run sequentially, use a single run_shell_command call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before run_shell_command for git operations, or git add before git commit), run these operations sequentially instead.
96-
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
97-
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
89+
- If the commands depend on each other and must run sequentially, use a single run_shell_command call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`).
90+
- Use '&' only when you need to run commands sequentially but don't care if earlier commands fail.
91+
- DO NOT use ';' or newlines to separate commands in cmd.exe.
9892
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
9993
<good-example>
10094
pytest /foo/bar/tests

packages/core/src/tools/shell.test.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.j
4040
import { PermissionManager } from '../permissions/permission-manager.js';
4141
import { CommitAttributionService } from '../services/commitAttribution.js';
4242

43+
interface ShellToolParameterJsonSchema {
44+
properties: {
45+
command: {
46+
description: string;
47+
};
48+
};
49+
}
50+
51+
function getCommandParameterDescription(shellTool: ShellTool): string {
52+
return (shellTool.schema.parametersJsonSchema as ShellToolParameterJsonSchema)
53+
.properties.command.description;
54+
}
55+
4356
describe('ShellTool', () => {
4457
let shellTool: ShellTool;
4558
let mockConfig: Config;
@@ -95,14 +108,14 @@ describe('ShellTool', () => {
95108
on: vi.fn(),
96109
} as unknown as fs.WriteStream);
97110

98-
shellTool = new ShellTool(mockConfig);
99-
100111
vi.mocked(os.platform).mockReturnValue('linux');
101112
vi.mocked(os.tmpdir).mockReturnValue('/tmp');
102113
(vi.mocked(crypto.randomBytes) as Mock).mockReturnValue(
103114
Buffer.from('abcdef', 'hex'),
104115
);
105116

117+
shellTool = new ShellTool(mockConfig);
118+
106119
// Capture the output callback to simulate streaming events from the service
107120
mockShellExecutionService.mockImplementation((_cmd, _cwd, callback) => {
108121
mockShellOutputCallback = callback;
@@ -3954,16 +3967,104 @@ describe('ShellTool', () => {
39543967
});
39553968

39563969
describe('getDescription', () => {
3970+
const originalEnv = { ...process.env };
3971+
3972+
afterEach(() => {
3973+
process.env = { ...originalEnv };
3974+
});
3975+
39573976
it('should return the windows description when on windows', async () => {
39583977
vi.mocked(os.platform).mockReturnValue('win32');
3978+
delete process.env['ComSpec'];
3979+
delete process.env['MSYSTEM'];
3980+
delete process.env['TERM'];
39593981
const shellTool = new ShellTool(mockConfig);
39603982
expect(shellTool.description).toMatchSnapshot();
3983+
expect(shellTool.description).toContain(
3984+
"Use '&' only when you need to run commands sequentially",
3985+
);
3986+
expect(shellTool.description).toContain(
3987+
"DO NOT use ';' or newlines to separate commands in cmd.exe.",
3988+
);
3989+
expect(getCommandParameterDescription(shellTool)).toBe(
3990+
'Exact cmd.exe command to execute as `cmd.exe /d /s /c <command>`',
3991+
);
39613992
});
39623993

39633994
it('should return the non-windows description when not on windows', async () => {
39643995
vi.mocked(os.platform).mockReturnValue('linux');
39653996
const shellTool = new ShellTool(mockConfig);
39663997
expect(shellTool.description).toMatchSnapshot();
3998+
expect(getCommandParameterDescription(shellTool)).toBe(
3999+
'Exact bash command to execute as `bash -c <command>`',
4000+
);
4001+
});
4002+
4003+
it('should describe PowerShell when ComSpec points to powershell.exe', async () => {
4004+
vi.mocked(os.platform).mockReturnValue('win32');
4005+
process.env['ComSpec'] =
4006+
'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
4007+
delete process.env['MSYSTEM'];
4008+
delete process.env['TERM'];
4009+
4010+
const shellTool = new ShellTool(mockConfig);
4011+
4012+
expect(shellTool.description).toContain(
4013+
'`powershell.exe -NoProfile -Command <command>`',
4014+
);
4015+
expect(shellTool.description).toContain(
4016+
'The active shell is PowerShell.',
4017+
);
4018+
expect(shellTool.description).toContain(
4019+
'Do NOT use Bash-only forms such as ANSI-C quoting',
4020+
);
4021+
expect(shellTool.description).toContain(
4022+
"Windows PowerShell does not support '&&'.",
4023+
);
4024+
expect(shellTool.description).not.toContain(
4025+
"use a single run_shell_command call with '&&'",
4026+
);
4027+
expect(getCommandParameterDescription(shellTool)).toBe(
4028+
'Exact PowerShell command to execute as `powershell.exe -NoProfile -Command <command>`',
4029+
);
4030+
});
4031+
4032+
it('should describe pwsh when ComSpec points to pwsh.exe', async () => {
4033+
vi.mocked(os.platform).mockReturnValue('win32');
4034+
process.env['ComSpec'] = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
4035+
delete process.env['MSYSTEM'];
4036+
delete process.env['TERM'];
4037+
4038+
const shellTool = new ShellTool(mockConfig);
4039+
4040+
expect(shellTool.description).toContain(
4041+
'`pwsh.exe -NoProfile -Command <command>`',
4042+
);
4043+
expect(shellTool.description).toContain(
4044+
"use a single run_shell_command call with '&&'",
4045+
);
4046+
expect(getCommandParameterDescription(shellTool)).toBe(
4047+
'Exact PowerShell command to execute as `pwsh.exe -NoProfile -Command <command>`',
4048+
);
4049+
});
4050+
4051+
it('should describe bash when Windows is running in Git Bash', async () => {
4052+
vi.mocked(os.platform).mockReturnValue('win32');
4053+
process.env['ComSpec'] = 'C:\\WINDOWS\\System32\\cmd.exe';
4054+
process.env['MSYSTEM'] = 'MINGW64';
4055+
delete process.env['TERM'];
4056+
4057+
const shellTool = new ShellTool(mockConfig);
4058+
4059+
expect(shellTool.description).toContain('`bash -c <command>`');
4060+
expect(shellTool.description).toContain('The active shell is Bash.');
4061+
expect(shellTool.description).toContain('ANSI-C quoting');
4062+
expect(shellTool.description).not.toContain(
4063+
'Command process group can be terminated',
4064+
);
4065+
expect(getCommandParameterDescription(shellTool)).toBe(
4066+
'Exact bash command to execute as `bash -c <command>`',
4067+
);
39674068
});
39684069
});
39694070

0 commit comments

Comments
 (0)