diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 9f471bab..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "permissions": { - "allow": [ - "WebFetch(domain:acpedia.org)" - ], - "deny": [ - "Bash", - "Write", - "Edit", - "Read", - "Glob", - "Grep", - "NotebookEdit", - "WebSearch" - ], - "ask": [] - }, - "enableAllProjectMcpServers": true -} diff --git a/.gitignore b/.gitignore index 5eb460cd..0696fc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ __pycache__ static/v2/ frontend/node_modules/ -# Claude Code per-machine permissions (do NOT deploy to server — production -# agent must run with the strict permissions in committed .claude/settings.json) -.claude/settings.local.json -.claude/settings.local.json.* +# Claude Code config — never commit. The production agent's strict +# permissions live server-side at /var/lib/overlord-agent/.claude/ +# (and via CLI flags in agent/claude_wrapper.py). The repo stays +# permission-neutral so devs can `claude` interactively here without +# inheriting production-agent restrictions. +.claude/ diff --git a/agent/claude_wrapper.py b/agent/claude_wrapper.py index af23432f..4ea483fd 100644 --- a/agent/claude_wrapper.py +++ b/agent/claude_wrapper.py @@ -91,6 +91,50 @@ async def ask_claude(message: str, session_id: str) -> ClaudeResult: ] ) + # CRITICAL: Claude Code's built-in meta-tools (ToolSearch, Monitor, etc.) + # bypass the --allowed-tools whitelist. They come from Anthropic's tool + # registry rather than from local MCP servers. We must explicitly DISALLOW + # them — confirmed by testing that ToolSearch was reachable even with + # `--permission-mode dontAsk` and a tight --allowed-tools list. + disallowed_tools = ",".join( + [ + # File / shell / search built-ins (defense in depth — already not + # in allow list, but if someone toggles permission-mode this + # belt-and-suspenders the deny side). + "Bash", + "Write", + "Edit", + "Read", + "Glob", + "Grep", + "NotebookEdit", + # Network built-ins + "WebSearch", + # Tool / session meta-tools — these can list, load, or chain + # into other tools and must NOT be reachable. + "ToolSearch", + "Monitor", + "TaskOutput", + "TaskStop", + "TodoWrite", + "Skill", + "EnterPlanMode", + "ExitPlanMode", + "EnterWorktree", + "ExitWorktree", + "AskUserQuestion", + "ListMcpResourcesTool", + "ReadMcpResourceTool", + "PushNotification", + # Scheduling / cron — the agent must never schedule itself. + "CronCreate", + "CronList", + "CronDelete", + "ScheduleWakeup", + "RemoteTrigger", + ] + ) + # Pick --session-id (creates) vs --resume (continues) based on whether # the session JSONL already exists on disk. is_new = not _session_exists(session_id) @@ -105,6 +149,10 @@ async def ask_claude(message: str, session_id: str) -> ClaudeResult: "json", "--allowed-tools", allowed_tools, + # Built-in meta-tools that --allowed-tools does NOT block — must + # be explicitly listed here. + "--disallowed-tools", + disallowed_tools, # CRITICAL: dontAsk auto-DENIES anything outside --allowed-tools. # Do NOT use bypassPermissions here — that mode ignores the whitelist # entirely and lets the model call Bash/Write/Edit/etc. (verified