From e780f249d1806106a05d7c1d7d602d0d1c3fc599 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 25 Apr 2026 22:26:02 +0200 Subject: [PATCH] fix(agent): keep strict permissions server-side, not in repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit put .claude/settings.json IN THE REPO, which would have applied its strict deny rules to ANY Claude Code invocation from this cwd — including the human user's interactive dev sessions on their own machine. That's wrong; the production agent's lockdown should not constrain the developer. Remove the committed file and gitignore .claude/ entirely. The repo is permission-neutral now. Strict permissions for the production agent come from two server-only sources: 1. CLI flags in agent/claude_wrapper.py (--allowed-tools + --disallowed-tools, passed by the systemd-spawned subprocess only) 2. /var/lib/overlord-agent/.claude/settings.json (the agent's own HOME — separate from any user's .claude/) Also bumps claude_wrapper.py with the explicit --disallowed-tools list of meta-tools (ToolSearch, Monitor, TodoWrite, TaskOutput, Skill, cron tools, etc.) that the --allowed-tools whitelist does not block on its own. Verified empirically: with only --allowed-tools, ToolSearch was still callable; --disallowed-tools is required. --- .claude/settings.json | 19 ---------------- .gitignore | 10 +++++---- agent/claude_wrapper.py | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 23 deletions(-) delete mode 100644 .claude/settings.json 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