Repository navigation
claude_code roles load the operator's user- and project-scope MCP servers; --strict-mcp-config is only a comment #74
Description
Summary
src/lh_harness/adapters/claude_code.py:65 states that role isolation keeps unrelated MCP servers
out of the agents:
# MCP support remains opt-in. --strict-mcp-config keeps unrelated
# user/project MCP servers out of every role.The flag is never added to command_parts. It occurs exactly once in the repository — in that
comment:
$ grep -rn 'strict-mcp-config\|strict_mcp' . --include='*.py' --include='*.md' --include='*.toml'
src/lh_harness/adapters/claude_code.py:65: # MCP support remains opt-in. --strict-mcp-config keeps unrelated
Without it the claude CLI loads its normal MCP configuration sources, so every MCP server the
operator has configured at user scope, plus any .mcp.json sitting in the workspace, is loaded into
the manager, executor, auditor and final-response episodes.
What happens
Measured from the system/init event in each role's captured claude_stream.jsonl of a real run
(lh-harness run --agent claude_code --max-rounds 1 --no-dashboard), on a workspace containing a
.mcp.json and on an account with one unrelated user-scope server:
=== manager_episodes ===
mcp_servers = [{'name': 'leak-probe', 'status': 'pending'},
{'name': '<unrelated user-scope server>', 'status': 'needs-auth'}]
=== cli_executor_episodes ===
mcp_servers = [{'name': 'leak-probe', 'status': 'pending'},
{'name': '<unrelated user-scope server>', 'status': 'needs-auth'}]
=== cli_auditor_episodes ===
mcp_servers = [{'name': 'leak-probe', 'status': 'pending'},
{'name': '<unrelated user-scope server>', 'status': 'needs-auth'}]
=== final_response_episodes ===
mcp_servers = [{'name': 'leak-probe', 'status': 'pending'},
{'name': '<unrelated user-scope server>', 'status': 'needs-auth'}]
leak-probe is a placeholder server declared only in the workspace .mcp.json; it was never passed
through --claude-mcp-config or LH_HARNESS_CLAUDECODE_MCP_CONFIG. Note that the workspace
.mcp.json is loaded without any approval prompt, because the adapter also passes
--dangerously-skip-permissions.
Two consequences, of different weight:
- Executor and auditor roles can call these servers' tools.
policy_for_roledeniesmcp__*
formanagerandfinal_response, but the executor deny-list is("Agent",)and the auditor
deny-list is the write tools plus"Agent"(src/lh_harness/adapters/claude_permissions.py:33-72).
So for the roles that actually act, an arbitrary unrelated MCP server is reachable — including
inside the auditor, whose value depends on it being a constrained observer. - Every role starts these servers and pays for their tool definitions in each episode's context,
for servers the run never asked for.
The read-only guard does not cover this: snapshot_workspace compares the workspace, and an MCP
server's side effects are typically not in the workspace at all.
Reproduction
No personal configuration needed:
mkdir /tmp/mcp-repro && cd /tmp/mcp-repro
printf 'alpha,3\nbeta,7\ngamma,5\n' > input.txt
cat > .mcp.json <<'JSON'
{"mcpServers": {"leak-probe": {"command": "/bin/sh", "args": ["-c", "sleep 3600"]}}}
JSON
lh-harness run --agent claude_code --model claude-sonnet-5 --max-rounds 1 --no-dashboard \
--task 'Read input.txt. Write total.txt containing only the sum of the numbers.'
python3 - <<'PY'
import json, pathlib
for p in sorted(pathlib.Path(".lh-harness").rglob("claude_stream.jsonl")):
for line in p.open():
d = json.loads(line)
if d.get("type") == "system" and d.get("subtype") == "init":
print(p.parent.parent.name, "->", d.get("mcp_servers"))
break
PYEvery role prints leak-probe. The same happens for user-scope servers, with no .mcp.json
present at all.
The CLI behaviour on its own, for isolation:
$ cd /tmp/mcp-repro
$ claude --print --output-format stream-json --verbose --dangerously-skip-permissions \
--model claude-sonnet-5 <<< "Reply with OK." # init event:
mcp_servers = [{'name': 'leak-probe', 'status': 'pending'}, ...]
$ claude ... --strict-mcp-config <<< "Reply with OK." # init event:
mcp_servers = []
Expected behaviour
What the comment says: a role sees only the servers in an explicit --mcp-config, and no servers at
all when none is configured. Adding --strict-mcp-config to command_parts produces exactly that;
with the flag, all four roles report mcp_servers = [] on the same reproduction, and the run
reaches the same outcome (total.txt written, same status and abort_reason).
Environment
- lh-harness 0.1.7, commit
a1dd930(currentmain) claude2.1.251- macOS 26.3 arm64, Python 3.14.6
--agent claude_code,--no-dashboard, no--claude-mcp-configand no
LH_HARNESS_CLAUDECODE_MCP_CONFIGset
Related
Distinct from #61, which opts Claude Code agents out of the operator's plugins and skills via
--setting-sources project. That is a larger, opt-in feature and does not add
--strict-mcp-config; MCP servers are a separate configuration source. In the run above, with the
MCP fix applied, the plugin/skill surface is unchanged (63 slash commands, 5 sub-agents still
visible), so the two changes are complementary rather than overlapping.
I have a one-line fix plus a regression test and can open a PR.
Activity
Upper9527 commentedon Oct 21, 2026
Fixed by merging #75. Every Claude Code role now passes
--strict-mcp-config, so unrelated user/project MCP registries are not implicitly loaded; explicit and environment-provided MCP configuration still work. The nine regression cases fail before the patch and pass afterward; the full combined suite passed (457 passed, 1 skipped). This is available on main and is separate from plugin/skill isolation. Thanks for the precise report and contribution.Upper9527 commentedon Oct 21, 2026
The Claude Code MCP isolation fix from #75 is now published on PyPI in v0.1.8. Every Claude Code role now uses
--strict-mcp-config. If your workflow previously relied on implicitly loaded user/project MCP servers, supply the intended MCP configuration explicitly.Upgrade with pip:
For a uv tool installation:
Verify the installed version with
lh-harness --version.v0.1.8 release notes