diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index b9e0f8a1c3..52aaf7e1bc 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -16,7 +16,6 @@ import { Shell } from "@/shell/shell"
import { BashArity } from "@/permission/arity"
-const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
export const log = Log.create({ service: "bash-tool" })
@@ -172,15 +171,13 @@ export const BashTool = Tool.define("bash", async () => {
})
const append = (chunk: Buffer) => {
- if (output.length <= MAX_OUTPUT_LENGTH) {
- output += chunk.toString()
- ctx.metadata({
- metadata: {
- output,
- description: params.description,
- },
- })
- }
+ output += chunk.toString()
+ ctx.metadata({
+ metadata: {
+ output,
+ description: params.description,
+ },
+ })
}
proc.stdout?.on("data", append)
@@ -228,12 +225,7 @@ export const BashTool = Tool.define("bash", async () => {
})
})
- let resultMetadata: String[] = [""]
-
- if (output.length > MAX_OUTPUT_LENGTH) {
- output = output.slice(0, MAX_OUTPUT_LENGTH)
- resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`)
- }
+ const resultMetadata: string[] = []
if (timedOut) {
resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`)
@@ -243,9 +235,8 @@ export const BashTool = Tool.define("bash", async () => {
resultMetadata.push("User aborted the command")
}
- if (resultMetadata.length > 1) {
- resultMetadata.push("")
- output += "\n\n" + resultMetadata.join("\n")
+ if (resultMetadata.length > 0) {
+ output += "\n\n\n" + resultMetadata.join("\n") + "\n"
}
return {
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 2eb17a9fc9..b58858f11d 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -4,6 +4,7 @@ import { BashTool } from "../../src/tool/bash"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import type { PermissionNext } from "../../src/permission/next"
+import { Truncate } from "../../src/tool/truncation"
const ctx = {
sessionID: "test",
@@ -230,3 +231,91 @@ describe("tool.bash permissions", () => {
})
})
})
+
+describe("tool.bash truncation", () => {
+ test("truncates output exceeding line limit", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const bash = await BashTool.init()
+ const lineCount = Truncate.MAX_LINES + 500
+ const result = await bash.execute(
+ {
+ command: `seq 1 ${lineCount}`,
+ description: "Generate lines exceeding limit",
+ },
+ ctx,
+ )
+ expect((result.metadata as any).truncated).toBe(true)
+ expect(result.output).toContain("truncated")
+ expect(result.output).toContain("Full output written to:")
+ },
+ })
+ })
+
+ test("truncates output exceeding byte limit", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const bash = await BashTool.init()
+ const byteCount = Truncate.MAX_BYTES + 10000
+ const result = await bash.execute(
+ {
+ command: `head -c ${byteCount} /dev/zero | tr '\\0' 'a'`,
+ description: "Generate bytes exceeding limit",
+ },
+ ctx,
+ )
+ expect((result.metadata as any).truncated).toBe(true)
+ expect(result.output).toContain("truncated")
+ expect(result.output).toContain("Full output written to:")
+ },
+ })
+ })
+
+ test("does not truncate small output", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const bash = await BashTool.init()
+ const result = await bash.execute(
+ {
+ command: "echo hello",
+ description: "Echo hello",
+ },
+ ctx,
+ )
+ expect((result.metadata as any).truncated).toBe(false)
+ expect(result.output).toBe("hello\n")
+ },
+ })
+ })
+
+ test("full output is saved to file when truncated", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const bash = await BashTool.init()
+ const lineCount = Truncate.MAX_LINES + 100
+ const result = await bash.execute(
+ {
+ command: `seq 1 ${lineCount}`,
+ description: "Generate lines for file check",
+ },
+ ctx,
+ )
+ expect((result.metadata as any).truncated).toBe(true)
+
+ const match = result.output.match(/Full output written to: (.+)/)
+ expect(match).toBeTruthy()
+
+ const filepath = match![1].split("\n")[0]
+ const saved = await Bun.file(filepath).text()
+ const lines = saved.trim().split("\n")
+ expect(lines.length).toBe(lineCount)
+ expect(lines[0]).toBe("1")
+ expect(lines[lineCount - 1]).toBe(String(lineCount))
+ },
+ })
+ })
+})