Policy: deny nested .claude/.git/.ssh/.aws/.config paths in the workspace
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 changed files +5 −3
modified
services/research-runner/src/policy.test.ts
+2 −0
@@ -77,6 +77,8 @@ describe("platform policy hook", () => { | |||||
| 77 | expect(isDeny(await call("Edit", { file_path: join(ws, "CLAUDE.md") }))).toBe(true); | 77 | expect(isDeny(await call("Edit", { file_path: join(ws, "CLAUDE.md") }))).toBe(true); | ||
| 78 | expect(isDeny(await call("Write", { file_path: join(ws, "input/run.json") }))).toBe(true); | 78 | expect(isDeny(await call("Write", { file_path: join(ws, "input/run.json") }))).toBe(true); | ||
| 79 | expect(isDeny(await call("Write", { file_path: join(ws, ".claude/settings.json") }))).toBe(true); | 79 | expect(isDeny(await call("Write", { file_path: join(ws, ".claude/settings.json") }))).toBe(true); | ||
| 80 | + expect(isDeny(await call("Write", { file_path: join(ws, "code/.claude/settings.json") }))).toBe(true); | ||||
| 81 | + expect(isDeny(await call("Write", { file_path: join(ws, "code/.git/hooks/pre-commit") }))).toBe(true); | ||||
| 80 | expect(isDeny(await call("Write", { file_path: join(ws, "results/link/evil.txt") }))).toBe(true); // symlink to outside | 82 | expect(isDeny(await call("Write", { file_path: join(ws, "results/link/evil.txt") }))).toBe(true); // symlink to outside | ||
| 81 | }); | 83 | }); | ||
| 82 | it("reviewer sessions may only write under review/", async () => { | 84 | it("reviewer sessions may only write under review/", async () => { | ||
modified
services/research-runner/src/policy.ts
+3 −3
@@ -202,10 +202,10 @@ export function createPolicyHook(opts: PolicyOptions): HookCallback { | |||||
| 202 | const rel = relativeInWorkspace(opts.workspace, p); | 202 | const rel = relativeInWorkspace(opts.workspace, p); | ||
| 203 | if (rel === null) return deny(input, "write outside the run workspace", { path: p }); | 203 | if (rel === null) return deny(input, "write outside the run workspace", { path: p }); | ||
| 204 | if ( | 204 | if ( | ||
| 205 | − rel.startsWith(".claude/") || | 205 | + rel.split("/").includes(".claude") || | ||
| 206 | − rel === ".claude" || | ||||
| 207 | rel.startsWith(".autoscholar/") || | 206 | rel.startsWith(".autoscholar/") || | ||
| 208 | − rel.startsWith("home/") | 207 | + rel.startsWith("home/") || | ||
| 208 | + rel.split("/").some((seg) => [".git", ".ssh", ".aws", ".config"].includes(seg)) | ||||
| 209 | ) | 209 | ) | ||
| 210 | return deny(input, "platform-reserved directory", { path: rel }); | 210 | return deny(input, "platform-reserved directory", { path: rel }); | ||
| 211 | for (const f of opts.frozenPaths) | 211 | for (const f of opts.frozenPaths) | ||
| 212 | 212 | ||||