diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 883810f9fc..0c72007083 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -196,6 +196,11 @@ class GitHubCopilotOptions(TypedDict, total=False): timeout: float """Request timeout in seconds. Defaults to GITHUB_COPILOT_TIMEOUT environment variable or 60 seconds.""" + enable_file_hooks: bool + """Whether the session reads checkout-local ``.github/hooks/`` files. Defaults to + False so behavior does not depend on the working directory the agent runs in; + set True (via default_options or per-run options) to opt into checkout-driven hooks.""" + log_level: str """CLI log level. Defaults to GITHUB_COPILOT_LOG_LEVEL environment variable.""" @@ -1232,6 +1237,11 @@ def _build_session_kwargs( ) kwargs["hooks"] = self._build_session_hooks(all_tools, kwargs) + # The SDK's default reads `.github/hooks/` from the process working + # directory, so identical agents would behave differently depending on + # where they were started. Pin it off unless the caller opts in. + kwargs.setdefault("enable_file_hooks", False) + # Strip agent-internal and client-level keys that are consumed here or in the # run methods (and settings) but are NOT valid create_session parameters, so # they don't leak through the passthrough layer and raise TypeError. diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index 5e4ff52e05..a4241c65da 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -1125,6 +1125,7 @@ async def test_session_resumed_for_same_session( model=unittest.mock.ANY, on_permission_request=unittest.mock.ANY, hooks=unittest.mock.ANY, + enable_file_hooks=unittest.mock.ANY, ) async def test_session_config_includes_model( @@ -1904,6 +1905,35 @@ def runtime_hook(_input: Any, _context: Any) -> Any: # on_pre_tool_use is still honored via the hooks parameter. assert config["hooks"]["on_pre_tool_use"] is runtime_hook + async def test_enable_file_hooks_defaults_to_false( + self, + mock_client: MagicMock, + ) -> None: + """Sessions must not depend on the cwd: file hooks default to off.""" + agent = GitHubCopilotAgent(client=mock_client) + await agent.start() + + await agent._get_or_create_session(AgentSession()) # type: ignore[reportPrivateUsage] + + config = mock_client.create_session.call_args.kwargs + assert config["enable_file_hooks"] is False + + async def test_enable_file_hooks_opt_in_wins( + self, + mock_client: MagicMock, + ) -> None: + """A caller who wants checkout-driven hooks can turn them back on.""" + agent = GitHubCopilotAgent( + client=mock_client, + default_options=cast(Any, {"enable_file_hooks": True}), + ) + await agent.start() + + await agent._get_or_create_session(AgentSession()) # type: ignore[reportPrivateUsage] + + config = mock_client.create_session.call_args.kwargs + assert config["enable_file_hooks"] is True + class TestGitHubCopilotAgentToolConversion: """Test cases for tool conversion.""" @@ -3667,3 +3697,4 @@ async def test_integration_run_with_shell_permissions_executes_command() -> None if isinstance(session.service_session_id, str) and agent._client: await agent._client.delete_session(session.service_session_id) +