Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions commands/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Parse `$ARGUMENTS` to identify what the user wants. If no arguments are given, s
- Allowed tools: (list or "default")
- Disallowed tools: (list or "none")

**Directories**
- Project: (current working directory)
- Additional: (list each path, or "none")

**Web UI**
- Enabled: yes/no
- Address: host:port
Expand Down Expand Up @@ -187,6 +191,40 @@ Configure web UI bind address or port.
3. Set `web.port` (number) or `web.host` (string) accordingly.
4. Write and confirm.

### `dirs` / `dirs list`

List current additional directories.

1. Read `.claude/claudeclaw/settings.json`.
2. Display the `additionalDirs` array, or "none" if empty.

### `dirs add <path1,path2,...>`

Add directories to the additional directories list.

1. Parse comma-separated absolute paths from `$ARGUMENTS`.
2. Validate each path is absolute and exists on disk.
3. Read `.claude/claudeclaw/settings.json`.
4. Append validated paths to `additionalDirs` (deduplicated).
5. Write and confirm.

### `dirs remove <path>`

Remove a directory from the additional directories list.

1. Parse the path from `$ARGUMENTS`.
2. Read `.claude/claudeclaw/settings.json`.
3. Filter the path out of `additionalDirs`.
4. Write and confirm.

### `dirs clear`

Clear all additional directories.

1. Read `.claude/claudeclaw/settings.json`.
2. Set `additionalDirs` to `[]`.
3. Write and confirm.

### `reset`

Reset all settings to defaults.
Expand Down Expand Up @@ -222,7 +260,8 @@ Reset all settings to defaults.
"enabled": false,
"host": "127.0.0.1",
"port": 4632
}
},
"additionalDirs": []
}
```
3. Confirm the reset. Note: this does not delete cron jobs — use `/heartbeat:jobs delete` for that.
Expand Down Expand Up @@ -264,7 +303,8 @@ Location: `.claude/claudeclaw/settings.json`
"enabled": true,
"host": "127.0.0.1",
"port": 4632
}
},
"additionalDirs": ["/Users/morgan/.ssh"]
}
```

Expand All @@ -291,5 +331,6 @@ Location: `.claude/claudeclaw/settings.json`
| `web.enabled` | boolean | Whether the web UI is served |
| `web.host` | string | Bind address (default `127.0.0.1`) |
| `web.port` | number | Port number (default `4632`) |
| `additionalDirs` | string[] | Absolute paths to directories Claude can access beyond the project root |

The daemon hot-reloads this file every 30 seconds. No restart needed after changes.
8 changes: 7 additions & 1 deletion commands/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ Start the heartbeat daemon for this project. Follow these steps exactly:
- "Allow any specific tools on top of the security level? (e.g. Bash(git:*) to allow only git commands)" (header: "Allow tools", options: "None — use level defaults (Recommended)", "Bash(git:*) — git only", "Bash(git:*) Bash(npm:*) — git + npm")
- If they pick an option with tools or type custom ones, set `security.allowedTools` to the list.

- **If security is NOT "unrestricted"**: Use AskUserQuestion to ask:
- "Want to grant access to directories outside this project? (e.g. ~/.ssh, shared config)" (header: "Additional Directories", options: "None", "Yes, I'll list them")
- If yes, ask in normal free-form text for absolute directory paths (one per line or comma-separated). Validate each path exists and is absolute. Set `additionalDirs` to the validated list.

Update `.claude/claudeclaw/settings.json` with their answers.

6. **Launch/start action**:
Expand Down Expand Up @@ -171,7 +175,8 @@ Defaults: `WEB_HOST=127.0.0.1`, `WEB_PORT=4632` unless changed via settings or `
"level": "moderate",
"allowedTools": [],
"disallowedTools": []
}
},
"additionalDirs": ["/Users/morgan/.ssh"]
}
```
- `model` — Claude model to use (`opus`, `sonnet`, `haiku`, `glm`, or full model ID). Empty string uses default.
Expand All @@ -187,6 +192,7 @@ Defaults: `WEB_HOST=127.0.0.1`, `WEB_PORT=4632` unless changed via settings or `
- `security.level` — one of: `locked`, `strict`, `moderate`, `unrestricted`
- `security.allowedTools` — extra tools to allow on top of the level (e.g. `["Bash(git:*)"]`)
- `security.disallowedTools` — tools to block on top of the level
- `additionalDirs` — absolute paths to directories Claude can access beyond the project root (only relevant when security is not `unrestricted`)

### Security Levels
All levels run without permission prompts (headless). Security is enforced via tool restrictions and project-directory scoping.
Expand Down
8 changes: 8 additions & 0 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ export async function start(args: string[] = []) {
console.log(` + allowed: ${settings.security.allowedTools.join(", ")}`);
if (settings.security.disallowedTools.length > 0)
console.log(` - blocked: ${settings.security.disallowedTools.join(", ")}`);
if (settings.additionalDirs.length > 0)
console.log(` Additional dirs: ${settings.additionalDirs.join(", ")}`);
console.log(` Heartbeat: ${settings.heartbeat.enabled ? `every ${settings.heartbeat.interval}m` : "disabled"}`);
console.log(` Web UI: ${webEnabled ? `http://${settings.web.host}:${webPort}` : "disabled"}`);
if (debugFlag) console.log(" Debug: enabled");
Expand Down Expand Up @@ -576,6 +578,12 @@ export async function start(args: string[] = []) {
console.log(`[${ts()}] Security level changed → ${newSettings.security.level}`);
}

const dirsChanged =
newSettings.additionalDirs.join(",") !== currentSettings.additionalDirs.join(",");
if (dirsChanged) {
console.log(`[${ts()}] Additional directories changed → [${newSettings.additionalDirs.join(", ")}]`);
}

if (hbChanged) {
console.log(`[${ts()}] Config change detected — heartbeat: ${newSettings.heartbeat.enabled ? `every ${newSettings.heartbeat.interval}m` : "disabled"}`);
currentSettings = newSettings;
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const DEFAULT_SETTINGS: Settings = {
security: { level: "moderate", allowedTools: [], disallowedTools: [] },
web: { enabled: false, host: "127.0.0.1", port: 4632 },
stt: { baseUrl: "", model: "" },
additionalDirs: [],
};

export interface HeartbeatExcludeWindow {
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface Settings {
security: SecurityConfig;
web: WebConfig;
stt: SttConfig;
additionalDirs: string[];
}

export interface ModelConfig {
Expand Down Expand Up @@ -157,6 +159,11 @@ function parseSettings(raw: Record<string, any>): Settings {
baseUrl: typeof raw.stt?.baseUrl === "string" ? raw.stt.baseUrl.trim() : "",
model: typeof raw.stt?.model === "string" ? raw.stt.model.trim() : "",
},
additionalDirs: Array.isArray(raw.additionalDirs)
? raw.additionalDirs
.filter((d: unknown) => typeof d === "string" && d.trim().length > 0 && isAbsolute(d.trim()))
.map((d: string) => d.trim())
: [],
};
}

Expand Down
30 changes: 22 additions & 8 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,22 @@ async function runClaudeOnce(

const PROJECT_DIR = process.cwd();

const DIR_SCOPE_PROMPT = [
`CRITICAL SECURITY CONSTRAINT: You are scoped to the project directory: ${PROJECT_DIR}`,
"You MUST NOT read, write, edit, or delete any file outside this directory.",
"You MUST NOT run bash commands that modify anything outside this directory (no cd /, no /etc, no ~/, no ../.. escapes).",
"If a request requires accessing files outside the project, refuse and explain why.",
].join("\n");
function buildDirScopePrompt(additionalDirs: string[]): string {
const parts = [
`CRITICAL SECURITY CONSTRAINT: You are scoped to the project directory: ${PROJECT_DIR}`,
];
if (additionalDirs.length > 0) {
parts.push(
`You also have access to these additional directories:\n${additionalDirs.map((d) => ` - ${d}`).join("\n")}`
);
}
parts.push(
"You MUST NOT read, write, edit, or delete any file outside these allowed directories.",
"You MUST NOT run bash commands that modify anything outside these directories.",
"If a request requires accessing files outside the allowed directories, refuse and explain why.",
);
return parts.join("\n");
}

export async function ensureProjectClaudeMd(): Promise<void> {
// Preflight-only initialization: never rewrite an existing project CLAUDE.md.
Expand Down Expand Up @@ -210,7 +220,7 @@ async function execClaude(name: string, prompt: string): Promise<RunResult> {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const logFile = join(LOGS_DIR, `${name}-${timestamp}.log`);

const { security, model, api, fallback } = getSettings();
const { security, model, api, fallback, additionalDirs } = getSettings();
const primaryConfig: ModelConfig = { model, api };
const fallbackConfig: ModelConfig = {
model: fallback?.model ?? "",
Expand All @@ -227,6 +237,10 @@ async function execClaude(name: string, prompt: string): Promise<RunResult> {
const outputFormat = isNew ? "json" : "text";
const args = ["claude", "-p", prompt, "--output-format", outputFormat, ...securityArgs];

for (const dir of additionalDirs) {
args.push("--add-dir", dir);
}

if (!isNew) {
args.push("--resume", existing.sessionId);
}
Expand All @@ -250,7 +264,7 @@ async function execClaude(name: string, prompt: string): Promise<RunResult> {
}
}

if (security.level !== "unrestricted") appendParts.push(DIR_SCOPE_PROMPT);
if (security.level !== "unrestricted") appendParts.push(buildDirScopePrompt(additionalDirs));
if (appendParts.length > 0) {
args.push("--append-system-prompt", appendParts.join("\n\n"));
}
Expand Down