fix(claude_session): surface real CLI error + sanitize nested env (#85)

write_interim_draft failed for all blocks from the CEO MCP instance with
"Claude CLI failed (exit 1): unknown error". Two fixes:

1. Error surfacing (the certain win): on non-zero exit, capture and log
   both stderr AND stdout (the CLI sometimes writes its diagnostic to
   stdout or nowhere), so the next occurrence is diagnosable instead of
   collapsing to "unknown error". This is why #85 was unsolved — the real
   error was swallowed (engineering rule §6: no silent swallow).

2. Defensive hardening: strip Claude Code session markers (CLAUDECODE,
   CLAUDE_CODE_*, CLAUDE_AGENT_*, AI_AGENT, CLAUDE_EFFORT) from the env of
   nested `claude -p` calls and run them from $HOME, decoupling them from
   the parent agent's session/project state. Aligns query() with the
   existing query_streaming() path (which already sets cwd=HOME). Auth/
   config vars are preserved.

Note: the original adapter-context failure could not be reproduced in a
plain interactive session (nested claude -p succeeds there in both old and
new code), so the env markers are a suspect, not a proven cause. The real
value is the diagnostics. Verified: nested query() returns PONG from
inside a CLAUDECODE=1 session; unit tests cover env sanitization.

Invariants: G1 (normalize at source — fix the spawn, not readers),
G2 (no parallel path — same query()), §6 (no silent error swallow).
INV: feedback_claude_session_local_only preserved (all calls stay local).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 19:29:36 +00:00
parent 37c00bac13
commit 8ec24cf822
2 changed files with 87 additions and 2 deletions

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import os
from legal_mcp.services import claude_session as cs
def test_clean_env_strips_session_markers(monkeypatch):
"""Nested claude -p must not inherit the parent session markers (#85)."""
for k in (
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CLAUDE_CODE_SESSION_ID",
"CLAUDE_CODE_EXECPATH",
"CLAUDE_CODE_SSE_PORT",
"CLAUDE_AGENT_SDK_VERSION",
"AI_AGENT",
"CLAUDE_EFFORT",
):
monkeypatch.setenv(k, "x")
env = cs._clean_subprocess_env()
assert "CLAUDECODE" not in env
assert "AI_AGENT" not in env
assert "CLAUDE_EFFORT" not in env
assert not any(k.startswith("CLAUDE_CODE_") for k in env)
assert not any(k.startswith("CLAUDE_AGENT_") for k in env)
def test_clean_env_keeps_auth_and_path(monkeypatch):
"""Auth/config + PATH/HOME must survive — they are needed by the CLI."""
monkeypatch.setenv("CLAUDECODE", "1")
monkeypatch.setenv("CLAUDE_CONFIG_DIR", "/home/chaim/.claude")
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://example")
monkeypatch.setenv("PATH", os.environ.get("PATH", "/usr/bin"))
env = cs._clean_subprocess_env()
# CLAUDE_CONFIG_DIR carries credentials — must NOT be stripped.
assert env.get("CLAUDE_CONFIG_DIR") == "/home/chaim/.claude"
assert env.get("ANTHROPIC_BASE_URL") == "https://example"
assert "PATH" in env
assert "CLAUDECODE" not in env