diff --git a/src/hooks/bash-command-compiler.ts b/src/hooks/bash-command-compiler.ts index 68e15342..12c2f4f7 100644 --- a/src/hooks/bash-command-compiler.ts +++ b/src/hooks/bash-command-compiler.ts @@ -177,6 +177,9 @@ function parseHeadTailStage(stage: string): { lineLimit: number; fromEnd: boolea return { lineLimit: Math.abs(count), fromEnd: cmd === "tail" }; } if (rest.length === 3 && rest[0] === "-n") { + // `tail -n +N` prints from line N onward, not the last N lines. Leave it to + // the VFS shell, whose tail implements the +N form. + if (cmd === "tail" && rest[1].startsWith("+")) return null; const count = Number(rest[1]); if (!Number.isFinite(count)) return null; return { lineLimit: Math.abs(count), fromEnd: cmd === "tail" }; diff --git a/tests/claude-code/bash-command-compiler.test.ts b/tests/claude-code/bash-command-compiler.test.ts index 3bb90a73..0337249d 100644 --- a/tests/claude-code/bash-command-compiler.test.ts +++ b/tests/claude-code/bash-command-compiler.test.ts @@ -454,4 +454,22 @@ describe("bash-command-compiler execution", () => { ); expect(output).toBe("/summaries/a.md"); }); + + it("does not compile `tail -n +N`, which starts at line N rather than keeping the last N lines", async () => { + expect(parseCompiledSegment("tail -n +3 /a.md")).toBeNull(); + + const readVirtualPathContentsFn = vi.fn(async () => new Map([["/a.md", "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8"]])); + const output = await executeCompiledBashCommand( + { query: vi.fn() } as any, + "memory", + "sessions", + "tail -n +3 /a.md", + { readVirtualPathContentsFn: readVirtualPathContentsFn as any }, + ); + // GNU tail prints l3..l8 here; the compiled fast path used to answer with the + // last three lines (l6..l8). Returning null hands the command to the VFS shell, + // whose tail implements the +N form. + expect(output).toBeNull(); + expect(readVirtualPathContentsFn).not.toHaveBeenCalled(); + }); });