From 467778af95db014e547e5eae4b6155134f2809b2 Mon Sep 17 00:00:00 2001 From: James Wiesebron Date: Fri, 24 Apr 2026 11:35:58 -0700 Subject: [PATCH 1/3] Fix #1993: default agent cwd to $EGG_REPO_PATH Sandbox agents started at HOME (/home/egg) instead of the repo directory (/home/egg/repos/), so early relative-path tool calls against .egg-state/... failed and agents wasted tokens rediscovering the layout. EGG_REPO_PATH was already in the container env; wire it in as the default cwd when no explicit cwd is passed. Covers both SDK paths that flow through run_agent_async (claude-sdk and the opt-in egg harness) and the harness factory's project CLAUDE.md lookup. Explicit cwd arguments still take precedence, and os.getcwd() remains the final fallback for local CLI use. Co-Authored-By: Claude Opus 4.7 (1M context) --- shared/egg_agent/client.py | 19 ++++++--- .../harness_factory.py | 4 +- tests/shared/egg_agent/test_client.py | 42 ++++++++++++++++++- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/shared/egg_agent/client.py b/shared/egg_agent/client.py index ebe674f958..2b28192e28 100644 --- a/shared/egg_agent/client.py +++ b/shared/egg_agent/client.py @@ -95,6 +95,12 @@ async def run_agent_async( """ model = model or DEFAULT_MODEL + # Resolve cwd: explicit arg > EGG_REPO_PATH > SDK default (os.getcwd()). + # Sandbox agents start at HOME (/home/egg) while the repo lives at + # /home/egg/repos/ (EGG_REPO_PATH). Defaulting to EGG_REPO_PATH + # lands the agent in the repo on its first tool call. See #1993. + resolved_cwd: str | None = str(cwd) if cwd else os.environ.get("EGG_REPO_PATH") + # --- Harness selection (opt-in via EGG_HARNESS env var) --- harness = os.environ.get("EGG_HARNESS", "claude-sdk") if harness == "egg": @@ -106,7 +112,7 @@ async def run_agent_async( model=model, max_turns=max_turns or 200, system_prompt=system_prompt, - cwd=str(cwd) if cwd else None, + cwd=resolved_cwd, timeout=timeout, on_output=on_output, env=env, @@ -194,7 +200,7 @@ async def _check_tool_permission( options = ClaudeAgentOptions( permission_mode="bypassPermissions", model=model, - cwd=str(cwd) if cwd else None, + cwd=resolved_cwd, env=env or {}, # Read CLAUDE.md and settings.json from the filesystem so the agent # picks up sandbox rules (BRC protocol, egg-orch CLI, git safety, etc.). @@ -275,10 +281,11 @@ async def _check_tool_permission( actual_model: str | None = None result_meta: dict[str, Any] = {} - # Log the effective cwd — when the caller did not pass one, the SDK - # inherits os.getcwd(), so log that rather than None. Keeps - # session-init lines diagnostically useful (see #1954). - effective_cwd = str(cwd) if cwd else os.getcwd() + # Log the effective cwd — when the caller did not pass one and + # EGG_REPO_PATH is unset, the SDK inherits os.getcwd(), so log + # that rather than None. Keeps session-init lines diagnostically + # useful (see #1954, #1993). + effective_cwd = resolved_cwd or os.getcwd() logger.info( "Agent session init", event_type="system", diff --git a/shared/egg_harness_integration/harness_factory.py b/shared/egg_harness_integration/harness_factory.py index ad25b298ce..44fc23c049 100644 --- a/shared/egg_harness_integration/harness_factory.py +++ b/shared/egg_harness_integration/harness_factory.py @@ -111,7 +111,9 @@ def create_egg_harness( provider = RetryProvider(inner_provider) # -- Tool registry ----------------------------------------------------- - cwd_str = str(cwd) if cwd is not None else None + # Default cwd to EGG_REPO_PATH so sandbox agents start inside the repo + # rather than at HOME (/home/egg). See #1993. + cwd_str = str(cwd) if cwd is not None else os.environ.get("EGG_REPO_PATH") registry = ToolRegistry() diff --git a/tests/shared/egg_agent/test_client.py b/tests/shared/egg_agent/test_client.py index bf295b9f43..8e2f9551ba 100644 --- a/tests/shared/egg_agent/test_client.py +++ b/tests/shared/egg_agent/test_client.py @@ -339,8 +339,12 @@ def test_structured_logging_init_and_result(self, mock_query): @patch("claude_agent_sdk.query", side_effect=_mock_query_success) def test_init_log_cwd_fallback(self, mock_query): - """Test that cwd falls back to os.getcwd() when not provided.""" - with patch("egg_agent.client.logger") as mock_logger: + """Test that cwd falls back to os.getcwd() when neither arg nor env is set.""" + with ( + patch("egg_agent.client.logger") as mock_logger, + patch.dict(os.environ, {}, clear=False) as env, + ): + env.pop("EGG_REPO_PATH", None) _run_async(run_agent_async("test prompt")) init_calls = [ @@ -365,6 +369,40 @@ def test_init_log_cwd_explicit(self, mock_query): assert len(init_calls) == 1 assert init_calls[0].kwargs["cwd"] == "/tmp/test-dir" + @patch("claude_agent_sdk.query", side_effect=_mock_query_success) + def test_init_log_cwd_falls_back_to_egg_repo_path(self, mock_query): + """When no cwd is passed, EGG_REPO_PATH should be used (see #1993).""" + with ( + patch("egg_agent.client.logger") as mock_logger, + patch.dict(os.environ, {"EGG_REPO_PATH": "/home/egg/repos/myrepo"}), + ): + _run_async(run_agent_async("test prompt")) + + init_calls = [ + c + for c in mock_logger.info.call_args_list + if c.args and c.args[0] == "Agent session init" + ] + assert len(init_calls) == 1 + assert init_calls[0].kwargs["cwd"] == "/home/egg/repos/myrepo" + + @patch("claude_agent_sdk.query", side_effect=_mock_query_success) + def test_explicit_cwd_wins_over_egg_repo_path(self, mock_query): + """Explicit cwd argument must take precedence over EGG_REPO_PATH.""" + with ( + patch("egg_agent.client.logger") as mock_logger, + patch.dict(os.environ, {"EGG_REPO_PATH": "/home/egg/repos/myrepo"}), + ): + _run_async(run_agent_async("test prompt", cwd="/tmp/explicit")) + + init_calls = [ + c + for c in mock_logger.info.call_args_list + if c.args and c.args[0] == "Agent session init" + ] + assert len(init_calls) == 1 + assert init_calls[0].kwargs["cwd"] == "/tmp/explicit" + @patch("claude_agent_sdk.query", side_effect=_mock_query_error) def test_structured_logging_on_error(self, mock_query): """Test that system/result log is emitted on error paths.""" From 4977e2c20f2220d65d872b442abf045e981c40af Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:49:33 +0000 Subject: [PATCH 2/3] Address review feedback: empty-string defense, redundancy comment, stronger assertions - Guard against empty-string EGG_REPO_PATH with 'or None' in both client.py and harness_factory.py so an empty env var is treated the same as unset. - Add comment documenting intentional redundancy of the EGG_REPO_PATH fallback at harness_factory.py:168 for direct callers. - Strengthen cwd tests to assert on the ClaudeAgentOptions.cwd actually passed to query(), not just the logged value. --- shared/egg_agent/client.py | 2 +- .../egg_harness_integration/harness_factory.py | 6 +++++- tests/shared/egg_agent/test_client.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/shared/egg_agent/client.py b/shared/egg_agent/client.py index 2b28192e28..4ac8bbe063 100644 --- a/shared/egg_agent/client.py +++ b/shared/egg_agent/client.py @@ -99,7 +99,7 @@ async def run_agent_async( # Sandbox agents start at HOME (/home/egg) while the repo lives at # /home/egg/repos/ (EGG_REPO_PATH). Defaulting to EGG_REPO_PATH # lands the agent in the repo on its first tool call. See #1993. - resolved_cwd: str | None = str(cwd) if cwd else os.environ.get("EGG_REPO_PATH") + resolved_cwd: str | None = str(cwd) if cwd else (os.environ.get("EGG_REPO_PATH") or None) # --- Harness selection (opt-in via EGG_HARNESS env var) --- harness = os.environ.get("EGG_HARNESS", "claude-sdk") diff --git a/shared/egg_harness_integration/harness_factory.py b/shared/egg_harness_integration/harness_factory.py index 44fc23c049..bb829ee587 100644 --- a/shared/egg_harness_integration/harness_factory.py +++ b/shared/egg_harness_integration/harness_factory.py @@ -113,7 +113,7 @@ def create_egg_harness( # -- Tool registry ----------------------------------------------------- # Default cwd to EGG_REPO_PATH so sandbox agents start inside the repo # rather than at HOME (/home/egg). See #1993. - cwd_str = str(cwd) if cwd is not None else os.environ.get("EGG_REPO_PATH") + cwd_str = str(cwd) if cwd is not None else (os.environ.get("EGG_REPO_PATH") or None) registry = ToolRegistry() @@ -165,6 +165,10 @@ def create_egg_harness( if system_prompt is None: # Look for project CLAUDE.md relative to cwd or EGG_REPO_PATH. project_claude_md: str | None = None + # cwd_str already incorporates the EGG_REPO_PATH fallback (line 116), + # so the `or` branch is redundant when called from client.py. It is + # kept for direct callers of create_egg_harness (e.g. validate_harness_parity.py) + # that may pass cwd=None without the env-var resolution layer. repo_path = cwd_str or os.environ.get("EGG_REPO_PATH") if repo_path: candidate = os.path.join(repo_path, "CLAUDE.md") diff --git a/tests/shared/egg_agent/test_client.py b/tests/shared/egg_agent/test_client.py index 8e2f9551ba..6ee1486906 100644 --- a/tests/shared/egg_agent/test_client.py +++ b/tests/shared/egg_agent/test_client.py @@ -355,6 +355,10 @@ def test_init_log_cwd_fallback(self, mock_query): assert len(init_calls) == 1 assert init_calls[0].kwargs["cwd"] == os.getcwd() + # Verify cwd on the options object actually passed to query() + options = mock_query.call_args.kwargs["options"] + assert options.cwd is None # SDK defaults to os.getcwd() + @patch("claude_agent_sdk.query", side_effect=_mock_query_success) def test_init_log_cwd_explicit(self, mock_query): """Test that cwd uses the provided value when passed explicitly.""" @@ -369,6 +373,10 @@ def test_init_log_cwd_explicit(self, mock_query): assert len(init_calls) == 1 assert init_calls[0].kwargs["cwd"] == "/tmp/test-dir" + # Verify cwd on the options object actually passed to query() + options = mock_query.call_args.kwargs["options"] + assert options.cwd == "/tmp/test-dir" + @patch("claude_agent_sdk.query", side_effect=_mock_query_success) def test_init_log_cwd_falls_back_to_egg_repo_path(self, mock_query): """When no cwd is passed, EGG_REPO_PATH should be used (see #1993).""" @@ -386,6 +394,10 @@ def test_init_log_cwd_falls_back_to_egg_repo_path(self, mock_query): assert len(init_calls) == 1 assert init_calls[0].kwargs["cwd"] == "/home/egg/repos/myrepo" + # Verify cwd on the options object actually passed to query() + options = mock_query.call_args.kwargs["options"] + assert options.cwd == "/home/egg/repos/myrepo" + @patch("claude_agent_sdk.query", side_effect=_mock_query_success) def test_explicit_cwd_wins_over_egg_repo_path(self, mock_query): """Explicit cwd argument must take precedence over EGG_REPO_PATH.""" @@ -403,6 +415,10 @@ def test_explicit_cwd_wins_over_egg_repo_path(self, mock_query): assert len(init_calls) == 1 assert init_calls[0].kwargs["cwd"] == "/tmp/explicit" + # Verify cwd on the options object actually passed to query() + options = mock_query.call_args.kwargs["options"] + assert options.cwd == "/tmp/explicit" + @patch("claude_agent_sdk.query", side_effect=_mock_query_error) def test_structured_logging_on_error(self, mock_query): """Test that system/result log is emitted on error paths.""" From 2de0500f0267409b77b8dfb34ce651f4a30b5d48 Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:01:01 +0000 Subject: [PATCH 3/3] Add test for empty EGG_REPO_PATH treated as unset --- tests/shared/egg_agent/test_client.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/shared/egg_agent/test_client.py b/tests/shared/egg_agent/test_client.py index 6ee1486906..f566f0a313 100644 --- a/tests/shared/egg_agent/test_client.py +++ b/tests/shared/egg_agent/test_client.py @@ -419,6 +419,29 @@ def test_explicit_cwd_wins_over_egg_repo_path(self, mock_query): options = mock_query.call_args.kwargs["options"] assert options.cwd == "/tmp/explicit" + @patch("claude_agent_sdk.query", side_effect=_mock_query_success) + def test_empty_egg_repo_path_treated_as_unset(self, mock_query): + """EGG_REPO_PATH='' (set but empty) should behave like unset.""" + with ( + patch("egg_agent.client.logger") as mock_logger, + patch.dict(os.environ, {"EGG_REPO_PATH": ""}), + ): + _run_async(run_agent_async("test prompt")) + + init_calls = [ + c + for c in mock_logger.info.call_args_list + if c.args and c.args[0] == "Agent session init" + ] + assert len(init_calls) == 1 + # Empty EGG_REPO_PATH should fall back to os.getcwd() + assert init_calls[0].kwargs["cwd"] == os.getcwd() + + # Verify cwd on the options object — should be None so SDK + # defaults to os.getcwd(), not "" + options = mock_query.call_args.kwargs["options"] + assert options.cwd is None + @patch("claude_agent_sdk.query", side_effect=_mock_query_error) def test_structured_logging_on_error(self, mock_query): """Test that system/result log is emitted on error paths."""