fix: allow quoted pipes in read-only bash parsing (#1194)

This commit is contained in:
Sarah Wooders
2026-02-27 15:40:58 -08:00
committed by GitHub
parent 81a826440c
commit 186483d750
4 changed files with 297 additions and 15 deletions

View File

@@ -430,6 +430,74 @@ test("plan mode - denies non-read-only Bash", () => {
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - allows Bash heredoc write to plan file", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const planPath = join(homedir(), ".letta", "plans", "unit-test-plan.md");
const command = `cat > ${planPath} <<'EOF'\n# Plan\n- step 1\nEOF`;
const result = checkPermission(
"Bash",
{ command },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - denies Bash heredoc write outside plans dir", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const command = "cat > /tmp/not-a-plan.md <<'EOF'\n# Plan\nEOF";
const result = checkPermission(
"Bash",
{ command },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - denies Bash heredoc write when extra commands follow", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const planPath = join(homedir(), ".letta", "plans", "unit-test-plan.md");
const command = `cat > ${planPath} <<'EOF'\n# Plan\nEOF\necho 'extra command'`;
const result = checkPermission(
"Bash",
{ command },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - allows read-only Bash commands", () => {
permissionMode.setMode("plan");
@@ -503,6 +571,18 @@ test("plan mode - allows read-only Bash commands", () => {
);
expect(chainedResult.decision).toBe("allow");
// quoted pipes in regex patterns should be treated as literals and allowed
const quotedPipeResult = checkPermission(
"Bash",
{
command:
'rg -n "memfs|memory filesystem|memory_filesystem|skills/|SKILL.md|git-backed|sync" letta tests -S',
},
permissions,
"/Users/test/project",
);
expect(quotedPipeResult.decision).toBe("allow");
// cd && dangerous command should still be denied
const cdDangerousResult = checkPermission(
"Bash",

View File

@@ -182,6 +182,15 @@ describe("isReadOnlyShellCommand", () => {
expect(isReadOnlyShellCommand("ls -la | grep txt | wc -l")).toBe(true);
});
test("allows pipe characters inside quoted args", () => {
expect(
isReadOnlyShellCommand(
'rg -n "memfs|memory filesystem|memory_filesystem|skills/|SKILL.md|git-backed|sync" letta tests -S',
),
).toBe(true);
expect(isReadOnlyShellCommand("grep 'foo|bar|baz' file.txt")).toBe(true);
});
test("blocks pipes with unsafe commands", () => {
expect(isReadOnlyShellCommand("cat file | rm")).toBe(false);
expect(isReadOnlyShellCommand("echo test | bash")).toBe(false);
@@ -203,6 +212,13 @@ describe("isReadOnlyShellCommand", () => {
test("blocks command substitution", () => {
expect(isReadOnlyShellCommand("echo $(rm file)")).toBe(false);
expect(isReadOnlyShellCommand("echo `rm file`")).toBe(false);
expect(isReadOnlyShellCommand('echo "$(rm file)"')).toBe(false);
expect(isReadOnlyShellCommand('echo "`rm file`"')).toBe(false);
});
test("allows literal redirects inside quotes", () => {
expect(isReadOnlyShellCommand('echo "a > b"')).toBe(true);
expect(isReadOnlyShellCommand("echo 'a >> b'")).toBe(true);
});
});