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_role denies mcp__*
for manager and final_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
PY
Every 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 (current main)
claude 2.1.251
- macOS 26.3 arm64, Python 3.14.6
--agent claude_code, --no-dashboard, no --claude-mcp-config and no
LH_HARNESS_CLAUDECODE_MCP_CONFIG set
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.
Summary
src/lh_harness/adapters/claude_code.py:65states that role isolation keeps unrelated MCP serversout of the agents:
The flag is never added to
command_parts. It occurs exactly once in the repository — in thatcomment:
Without it the
claudeCLI loads its normal MCP configuration sources, so every MCP server theoperator has configured at user scope, plus any
.mcp.jsonsitting in the workspace, is loaded intothe manager, executor, auditor and final-response episodes.
What happens
Measured from the
system/initevent in each role's capturedclaude_stream.jsonlof a real run(
lh-harness run --agent claude_code --max-rounds 1 --no-dashboard), on a workspace containing a.mcp.jsonand on an account with one unrelated user-scope server:leak-probeis a placeholder server declared only in the workspace.mcp.json; it was never passedthrough
--claude-mcp-configorLH_HARNESS_CLAUDECODE_MCP_CONFIG. Note that the workspace.mcp.jsonis loaded without any approval prompt, because the adapter also passes--dangerously-skip-permissions.Two consequences, of different weight:
policy_for_roledeniesmcp__*for
managerandfinal_response, but the executor deny-list is("Agent",)and the auditordeny-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.
for servers the run never asked for.
The read-only guard does not cover this:
snapshot_workspacecompares the workspace, and an MCPserver's side effects are typically not in the workspace at all.
Reproduction
No personal configuration needed:
Every role prints
leak-probe. The same happens for user-scope servers, with no.mcp.jsonpresent at all.
The CLI behaviour on its own, for isolation:
Expected behaviour
What the comment says: a role sees only the servers in an explicit
--mcp-config, and no servers atall when none is configured. Adding
--strict-mcp-configtocommand_partsproduces exactly that;with the flag, all four roles report
mcp_servers = []on the same reproduction, and the runreaches the same outcome (
total.txtwritten, samestatusandabort_reason).Environment
a1dd930(currentmain)claude2.1.251--agent claude_code,--no-dashboard, no--claude-mcp-configand noLH_HARNESS_CLAUDECODE_MCP_CONFIGsetRelated
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 theMCP 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.