From 9605e7f4beebd7a2ceadafef11625759ff4d3be5 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 5 Aug 2026 05:41:18 +0800 Subject: [PATCH] Python: pin enable_file_hooks off by default in Copilot sessions The Copilot SDK's default reads `.github/hooks/` from the process working directory, so the same agent with the same options behaved differently depending on where the process started, with no way for a caller to see or control it. `_build_session_kwargs` now pins enable_file_hooks to False unless the caller opts in through default_options or per-run options, and the key is documented in GitHubCopilotOptions. Options that only shape prompt context, such as enable_host_git_operations, are untouched. Fixes #7516 --- .../agent_framework_github_copilot/_agent.py | 10 ++++++ .../tests/test_github_copilot_agent.py | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) 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) +