Skip to content

Commit 16931a0

Browse files
authored
🤖 Block standalone sleep commands in bash tool (#262)
Blocks `sleep` at the start of bash commands to prevent agents from wasting time waiting. Agents are instructed via error message to use polling loops instead (`while ! condition; do sleep 1; done`). **Pattern:** `/^\s*sleep\s/` - Only blocks sleep at the beginning, not in loops or after other commands. **Examples:** - Blocked: `sleep 5` - Allowed: `while ! test -f done; do sleep 1; done` - Allowed: `echo start; sleep 2` Tests verify blocking behavior and confirm sleep works correctly in polling loops. _Generated with `cmux`_
1 parent ba7b2c5 commit 16931a0

File tree

7 files changed

+66
-17
lines changed

7 files changed

+66
-17
lines changed

src/services/tools/bash.test.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ describe("bash tool", () => {
268268
using testEnv = createTestBashTool();
269269
const tool = testEnv.tool;
270270
const args: BashToolArgs = {
271-
script: "sleep 10",
271+
script: "while true; do sleep 0.1; done",
272272
timeout_secs: 1,
273273
};
274274

@@ -507,15 +507,15 @@ describe("bash tool", () => {
507507

508508
const args: BashToolArgs = {
509509
// Background process that would block if we waited for it
510-
script: "sleep 100 > /dev/null 2>&1 &",
510+
script: "while true; do sleep 1; done > /dev/null 2>&1 &",
511511
timeout_secs: 5,
512512
};
513513

514514
const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult;
515515
const duration = performance.now() - startTime;
516516

517517
expect(result.success).toBe(true);
518-
// Should complete in well under 1 second, not wait for sleep 100
518+
// Should complete in well under 1 second, not wait for infinite loop
519519
expect(duration).toBeLessThan(2000);
520520
});
521521

@@ -527,7 +527,7 @@ describe("bash tool", () => {
527527
const args: BashToolArgs = {
528528
// Spawn background process, echo its PID, then exit
529529
// Should not wait for the background process
530-
script: "sleep 100 > /dev/null 2>&1 & echo $!",
530+
script: "while true; do sleep 1; done > /dev/null 2>&1 & echo $!",
531531
timeout_secs: 5,
532532
};
533533

@@ -550,7 +550,7 @@ describe("bash tool", () => {
550550

551551
const args: BashToolArgs = {
552552
// Background process with output redirected but still blocking
553-
script: "sleep 10 & wait",
553+
script: "while true; do sleep 0.1; done & wait",
554554
timeout_secs: 1,
555555
};
556556

@@ -655,6 +655,44 @@ describe("bash tool", () => {
655655
}
656656
});
657657

658+
it("should block sleep command at start of script", async () => {
659+
using testEnv = createTestBashTool();
660+
const tool = testEnv.tool;
661+
const args: BashToolArgs = {
662+
script: "sleep 5",
663+
timeout_secs: 10,
664+
};
665+
666+
const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult;
667+
668+
expect(result.success).toBe(false);
669+
if (!result.success) {
670+
expect(result.error).toContain("sleep commands are blocked");
671+
expect(result.error).toContain("polling loops");
672+
expect(result.error).toContain("while ! condition");
673+
expect(result.exitCode).toBe(-1);
674+
expect(result.wall_duration_ms).toBe(0);
675+
}
676+
});
677+
678+
it("should allow sleep in polling loops", async () => {
679+
using testEnv = createTestBashTool();
680+
const tool = testEnv.tool;
681+
const args: BashToolArgs = {
682+
script: "for i in 1 2 3; do echo $i; sleep 0.1; done",
683+
timeout_secs: 5,
684+
};
685+
686+
const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult;
687+
688+
expect(result.success).toBe(true);
689+
if (result.success) {
690+
expect(result.output).toContain("1");
691+
expect(result.output).toContain("2");
692+
expect(result.output).toContain("3");
693+
}
694+
});
695+
658696
it("should use default timeout (3s) when timeout_secs is undefined", async () => {
659697
using testEnv = createTestBashTool();
660698
const tool = testEnv.tool;

src/services/tools/bash.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
5252
};
5353
}
5454

55+
// Block sleep at the beginning of commands - they waste time waiting. Use polling loops instead.
56+
if (/^\s*sleep\s/.test(script)) {
57+
return {
58+
success: false,
59+
error:
60+
"sleep commands are blocked to minimize waiting time. Instead, use polling loops to check conditions repeatedly (e.g., 'while ! condition; do sleep 1; done' or 'until condition; do sleep 1; done').",
61+
exitCode: -1,
62+
wall_duration_ms: 0,
63+
};
64+
}
65+
5566
// Default timeout to 3 seconds for interactivity
5667
// OpenAI models often don't provide timeout_secs even when marked required,
5768
// so we make it optional with a sensible default.

tests/ipcMain/executeBash.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ describeIntegration("IpcMain executeBash integration tests", () => {
143143
const timeoutResult = await env.mockIpcRenderer.invoke(
144144
IPC_CHANNELS.WORKSPACE_EXECUTE_BASH,
145145
workspaceId,
146-
"sleep 10",
146+
"while true; do sleep 0.1; done",
147147
{ timeout_secs: 1 }
148148
);
149149

tests/ipcMain/renameWorkspace.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ describeIntegration("IpcMain rename workspace integration tests", () => {
247247
void sendMessageWithModel(
248248
env.mockIpcRenderer,
249249
workspaceId,
250-
"Run this bash command: sleep 30 && echo done"
250+
"Run this bash command: for i in {1..60}; do sleep 0.5; done && echo done"
251251
);
252252

253253
// Wait for stream to start

tests/ipcMain/resumeStream.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describeIntegration("IpcMain resumeStream integration tests", () => {
3535
void sendMessageWithModel(
3636
env.mockIpcRenderer,
3737
workspaceId,
38-
`Run this bash command: sleep 5 && echo '${expectedWord}'`,
38+
`Run this bash command: for i in 1 2 3; do sleep 0.5; done && echo '${expectedWord}'`,
3939
"anthropic",
4040
"claude-sonnet-4-5"
4141
);

tests/ipcMain/sendMessage.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
8888
const { env, workspaceId, cleanup } = await setupWorkspace(provider);
8989
try {
9090
// Start a long-running stream with a bash command that takes time
91-
const longMessage = "Run this bash command: sleep 60 && echo done";
91+
const longMessage = "Run this bash command: while true; do sleep 1; done";
9292
void sendMessageWithModel(env.mockIpcRenderer, workspaceId, longMessage, provider, model);
9393

9494
// Wait for stream to start
@@ -263,11 +263,11 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
263263

264264
const { env, workspaceId, cleanup } = await setupWorkspace(provider);
265265
try {
266-
// Start a stream with tool call that takes 10 seconds
266+
// Start a stream with tool call that takes a long time
267267
void sendMessageWithModel(
268268
env.mockIpcRenderer,
269269
workspaceId,
270-
"Run this bash command: sleep 10",
270+
"Run this bash command: while true; do sleep 0.1; done",
271271
provider,
272272
model
273273
);
@@ -279,7 +279,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
279279

280280
await collector1.waitForEvent("tool-call-start", 10000);
281281

282-
// At this point, bash sleep is running (will take 10 seconds if abort doesn't work)
282+
// At this point, bash loop is running (will run forever if abort doesn't work)
283283
// Get message ID for verification
284284
collector1.collect();
285285
const messageId =
@@ -344,8 +344,8 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
344344
expect(partialMessages.length).toBe(0);
345345
}
346346

347-
// Note: If test completes quickly (~5s), abort signal worked and killed sleep
348-
// If test takes ~10s, abort signal didn't work and sleep ran to completion
347+
// Note: If test completes quickly (~5s), abort signal worked and killed the loop
348+
// If test takes much longer, abort signal didn't work
349349
} finally {
350350
await cleanup();
351351
}
@@ -448,7 +448,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
448448
const result1 = await sendMessageWithModel(
449449
env.mockIpcRenderer,
450450
workspaceId,
451-
"Run this bash command: sleep 10 && echo done",
451+
"Run this bash command: for i in {1..20}; do sleep 0.5; done && echo done",
452452
provider,
453453
model
454454
);
@@ -467,7 +467,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => {
467467
const result2 = await sendMessageWithModel(
468468
env.mockIpcRenderer,
469469
workspaceId,
470-
"Run this bash command: sleep 5 && echo second",
470+
"Run this bash command: for i in {1..10}; do sleep 0.5; done && echo second",
471471
provider,
472472
model,
473473
{ editMessageId: (firstUserMessage as { id: string }).id }

tests/ipcMain/truncate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describeIntegration("IpcMain truncate integration tests", () => {
261261
void sendMessageWithModel(
262262
env.mockIpcRenderer,
263263
workspaceId,
264-
"Run this bash command: sleep 30 && echo done"
264+
"Run this bash command: for i in {1..60}; do sleep 0.5; done && echo done"
265265
);
266266

267267
// Wait for stream to start

0 commit comments

Comments
 (0)