fix(agent): keep strict permissions server-side, not in repo

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.
This commit is contained in:
Erik 2026-04-25 22:26:02 +02:00
parent f894399165
commit e780f249d1
3 changed files with 54 additions and 23 deletions

View file

@ -1,19 +0,0 @@
{
"permissions": {
"allow": [
"WebFetch(domain:acpedia.org)"
],
"deny": [
"Bash",
"Write",
"Edit",
"Read",
"Glob",
"Grep",
"NotebookEdit",
"WebSearch"
],
"ask": []
},
"enableAllProjectMcpServers": true
}

10
.gitignore vendored
View file

@ -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/

View file

@ -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