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 | 77 | expect(isDeny(await call("Edit", { file_path: join(ws, "CLAUDE.md") }))).toBe(true); |
| 78 | 78 | expect(isDeny(await call("Write", { file_path: join(ws, "input/run.json") }))).toBe(true); |
| 79 | 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 | 82 | expect(isDeny(await call("Write", { file_path: join(ws, "results/link/evil.txt") }))).toBe(true); // symlink to outside |
| 81 | 83 | }); |
| 82 | 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 | 202 | const rel = relativeInWorkspace(opts.workspace, p); |
| 203 | 203 | if (rel === null) return deny(input, "write outside the run workspace", { path: p }); |
| 204 | 204 | if ( |
| 205 | − rel.startsWith(".claude/") || | |
| 206 | − rel === ".claude" || | |
| 205 | + rel.split("/").includes(".claude") || | |
| 207 | 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 | 210 | return deny(input, "platform-reserved directory", { path: rel }); |
| 211 | 211 | for (const f of opts.frozenPaths) |
| 212 | 212 | |