diff --git a/src/agent/identity.ts b/src/agent/identity.ts index 3394348..66fa376 100644 --- a/src/agent/identity.ts +++ b/src/agent/identity.ts @@ -172,23 +172,39 @@ export function createClaudeConfig( // Create directory structure fs.mkdirSync(commandsDir, { recursive: true }); - // Base allow list — always includes workspace-local operations - const allow: string[] = ["Read", "Write(./**)", "Edit(./**)", "Bash(git diff *)"]; + const mappings = pathMappings ?? []; - // Add explicit read/write/edit permissions for each mapped host path - for (const mapping of pathMappings ?? []) { + // Build allow list — always workspace-local, plus explicit mapped host paths. + // When path mappings exist we use explicit Read(./**) instead of a blanket + // Read, because a blanket Read combined with deny(../**) would silently block + // the mapped absolute paths (deny always wins over allow in Claude Code). + const allow: string[] = [ + "Read(./**)", + "Write(./**)", + "Edit(./**)", + "Bash(git diff *)", + ]; + + for (const mapping of mappings) { const normalized = mapping.host.replace(/\\/g, "/"); allow.push(`Read(${normalized}/**)`); allow.push(`Write(${normalized}/**)`); allow.push(`Edit(${normalized}/**)`); } + // Deny list — the ../** rules are only added when there are NO path mappings. + // With mappings, deny(../**) would override the explicit host-path allows. + // Without mappings, ../** acts as a catch-all traversal guard. + const denyTraversal = mappings.length === 0 + ? ["Read(../**)", "Write(../**)", "Edit(../**)"] + : []; + const settings = { permissions: { allow, ask: ["Bash(git push *)"], deny: [ - "Read(../**)", "Write(../**)", "Edit(../**)", + ...denyTraversal, "Read(./.env)", "WebFetch", "Bash(rm -rf *)", "Bash(curl * | sh)", "Bash(ssh *)", "Bash(scp *)", diff --git a/tests/unit/identity-templates.test.ts b/tests/unit/identity-templates.test.ts index 685220a..b65585c 100644 --- a/tests/unit/identity-templates.test.ts +++ b/tests/unit/identity-templates.test.ts @@ -30,11 +30,11 @@ describe("identity-templates", () => { expect(settings.permissions.deny).toContain("Read(./.env)"); }); - it("settings.json permissions.allow contains Read", () => { + it("settings.json permissions.allow contains Read(./**)", () => { setupAgentWorkspace(tmpDir, "test-agent", "A test agent", "chan-123"); const settingsPath = path.join(tmpDir, ".claude", "settings.json"); const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); - expect(settings.permissions.allow).toContain("Read"); + expect(settings.permissions.allow).toContain("Read(./**)"); }); it("CLAUDE.md contains Prompt-Injection section", () => {