feat(DIS-152): mapped project paths (host:virtual) for agents #66
2 changed files with 23 additions and 7 deletions
|
|
@ -172,23 +172,39 @@ export function createClaudeConfig(
|
||||||
// Create directory structure
|
// Create directory structure
|
||||||
fs.mkdirSync(commandsDir, { recursive: true });
|
fs.mkdirSync(commandsDir, { recursive: true });
|
||||||
|
|
||||||
// Base allow list — always includes workspace-local operations
|
const mappings = pathMappings ?? [];
|
||||||
const allow: string[] = ["Read", "Write(./**)", "Edit(./**)", "Bash(git diff *)"];
|
|
||||||
|
|
||||||
// Add explicit read/write/edit permissions for each mapped host path
|
// Build allow list — always workspace-local, plus explicit mapped host paths.
|
||||||
for (const mapping of pathMappings ?? []) {
|
// 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, "/");
|
const normalized = mapping.host.replace(/\\/g, "/");
|
||||||
allow.push(`Read(${normalized}/**)`);
|
allow.push(`Read(${normalized}/**)`);
|
||||||
allow.push(`Write(${normalized}/**)`);
|
allow.push(`Write(${normalized}/**)`);
|
||||||
allow.push(`Edit(${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 = {
|
const settings = {
|
||||||
permissions: {
|
permissions: {
|
||||||
allow,
|
allow,
|
||||||
ask: ["Bash(git push *)"],
|
ask: ["Bash(git push *)"],
|
||||||
deny: [
|
deny: [
|
||||||
"Read(../**)", "Write(../**)", "Edit(../**)",
|
...denyTraversal,
|
||||||
"Read(./.env)", "WebFetch",
|
"Read(./.env)", "WebFetch",
|
||||||
"Bash(rm -rf *)", "Bash(curl * | sh)",
|
"Bash(rm -rf *)", "Bash(curl * | sh)",
|
||||||
"Bash(ssh *)", "Bash(scp *)",
|
"Bash(ssh *)", "Bash(scp *)",
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,11 @@ describe("identity-templates", () => {
|
||||||
expect(settings.permissions.deny).toContain("Read(./.env)");
|
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");
|
setupAgentWorkspace(tmpDir, "test-agent", "A test agent", "chan-123");
|
||||||
const settingsPath = path.join(tmpDir, ".claude", "settings.json");
|
const settingsPath = path.join(tmpDir, ".claude", "settings.json");
|
||||||
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
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", () => {
|
it("CLAUDE.md contains Prompt-Injection section", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue