Skip to content

Commit 462fa2f

Browse files
cdrury526claude
andauthored
fix(claude_code): stop echoed system prompt from short-circuiting trust dialog (#319)
The startup handler treated any "> "/"❯ " in the capture buffer as the idle prompt and returned early. The injected --append-system-prompt contains a line starting with "> `memory_store`", which the shell echoes into the capture buffer ~300ms before the workspace-trust dialog renders. The handler matched that marker, declared Claude Code ready, and returned before accepting the trust dialog. initialize() then blocked on {IDLE, COMPLETED} for 30s and the session was killed, so `cao launch` failed on every not-yet-trusted working directory. Remove the bare IDLE_PROMPT_PATTERN early-return. The version banner ("Welcome to" / "Claude Code v<n>") is the only ready signal that cannot appear in the echoed launch command; the trust and bypass dialogs are still handled explicitly above, and wait_until_status() remains the real readiness gate. Add a regression test that returns the echoed command on the first poll and the trust dialog on the second, asserting the trust dialog is accepted (the old early-return would skip it). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 17ae1f1 commit 462fa2f

2 files changed

Lines changed: 45 additions & 12 deletions

File tree

src/cli_agent_orchestrator/providers/claude_code.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,22 @@ def _handle_startup_prompts(self, timeout: Optional[float] = None) -> None:
338338
get_backend().send_special_key(self.session_name, self.window_name, "Enter")
339339
return
340340

341-
# 3) Claude Code fully started — no prompts needed
341+
# 3) Claude Code fully started — no prompts needed.
342+
# The version banner is the ONLY reliable "ready" signal here: it
343+
# renders only once the REPL is up and cannot appear in the echoed
344+
# launch command. The old bare IDLE_PROMPT_PATTERN ("> "/"❯ ") check
345+
# was removed: the injected --append-system-prompt text contains
346+
# "> `memory_store`" (start of a line), which the echoed command
347+
# surfaces in the capture buffer within ~300ms and false-matches as
348+
# "idle". The handler then returned BEFORE the workspace-trust dialog
349+
# rendered, leaving it unaccepted; initialize() then blocked on
350+
# {IDLE, COMPLETED} for 30s and the session was killed. Trust/bypass
351+
# dialogs are handled explicitly above; if no banner ever appears the
352+
# loop just waits out its timeout and the downstream
353+
# wait_until_status() remains the real readiness gate.
342354
if re.search(r"Welcome to|Claude Code v\d+", clean_output):
343355
logger.info("Claude Code started without prompts")
344356
return
345-
if re.search(IDLE_PROMPT_PATTERN, clean_output):
346-
logger.info("Claude Code idle prompt detected, no prompts needed")
347-
return
348357

349358
time.sleep(1.0)
350359
logger.warning("Startup prompt handler timed out")

test/providers/test_claude_code_coverage.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Additional tests for ClaudeCodeProvider to cover uncovered branches.
22
33
Covers: McpServer model_dump path, bypass permissions prompt handling,
4-
and idle prompt early return in _handle_startup_prompts.
4+
and the workspace-trust handling in _handle_startup_prompts (including the
5+
regression where the echoed launch command false-matched the idle prompt).
56
"""
67

78
import re
@@ -66,16 +67,39 @@ def test_bypass_permissions_prompt(self, mock_backend, provider):
6667
provider.session_name, provider.window_name, "Enter"
6768
)
6869

70+
@patch("cli_agent_orchestrator.providers.claude_code.time.sleep", lambda *a, **k: None)
6971
@patch("cli_agent_orchestrator.backends.registry._backend")
70-
def test_idle_prompt_detected_early_return(self, mock_backend, provider):
71-
"""When idle prompt is visible, returns immediately without sending keys."""
72-
from cli_agent_orchestrator.providers.claude_code import IDLE_PROMPT_PATTERN
73-
74-
mock_backend.get_history.return_value = "❯ "
72+
def test_echoed_prompt_does_not_short_circuit_trust(self, mock_backend, provider):
73+
"""Regression: the injected --append-system-prompt contains a line that
74+
starts with "> `memory_store`". The shell echoes the launch command into
75+
the capture buffer ~300ms before the workspace-trust dialog renders, so
76+
that "> " must NOT be treated as the idle prompt and end startup handling
77+
early — otherwise the trust dialog is left unaccepted and initialize()
78+
blocks on {IDLE, COMPLETED} until it times out and the session is killed.
79+
80+
First poll returns the echoed command (with the "> memory_store" marker
81+
but no dialog yet); second poll returns the trust dialog. The handler must
82+
accept the trust dialog (Enter) rather than returning on the first frame.
83+
"""
84+
echoed_launch_cmd = (
85+
"user@host:/tmp/proj$ claude --dangerously-skip-permissions "
86+
"--append-system-prompt '## Memory\n"
87+
"> `memory_store` and `memory_recall` are CAO's memory tools'"
88+
)
89+
trust_frame = (
90+
"Quick safety check: Is this a project you created or one you trust?\n"
91+
"❯ 1. Yes, I trust this folder\n"
92+
" 2. No, exit\n"
93+
)
94+
mock_backend.get_history.side_effect = [echoed_launch_cmd, trust_frame]
7595

76-
provider._handle_startup_prompts(timeout=1.0)
96+
provider._handle_startup_prompts(timeout=5.0)
7797

78-
# No exception means early return worked
98+
# Trust dialog accepted via Enter — proves we did not early-return on the
99+
# echoed "> memory_store" marker.
100+
mock_backend.send_special_key.assert_called_once_with(
101+
provider.session_name, provider.window_name, "Enter"
102+
)
79103

80104
@patch("cli_agent_orchestrator.backends.registry._backend")
81105
def test_welcome_banner_detected_early_return(self, mock_backend, provider):

0 commit comments

Comments
 (0)