-
Notifications
You must be signed in to change notification settings - Fork 1k
Open
Labels
mcsruntimeRequires a change in the copilot-agent-runtime repoRequires a change in the copilot-agent-runtime reporuntime triageTriggers automated runtime triage workflowTriggers automated runtime triage workflow
Description
Bug: preToolUse hooks not invoked for Anthropic BYOM sessions
Summary
The Copilot CLI does not invoke preToolUse hooks when a session uses an Anthropic BYOM provider (provider.type = "anthropic" with a custom base_url). Hooks work correctly for:
- OpenAI BYOM sessions (custom
base_url) - Both OpenAI and Anthropic sessions via the GitHub Copilot API (no custom provider)
Environment
github-copilot-sdk(Python): 0.1.32- Copilot CLI binary (
copilot/bin/VERSION): 1.0.2 - Python: 3.12
Reproduction
Self-contained script with a mock LLM server — no external services needed. The mock returns a single view tool call in the appropriate format (OpenAI or Anthropic). The hook denies all tool calls.
import asyncio, json, threading, uuid
from http.server import HTTPServer, BaseHTTPRequestHandler
from copilot import CopilotClient, PermissionHandler
PORT = 18901
class MockLLM(BaseHTTPRequestHandler):
"""Returns a view(/app) tool call once, then plain text."""
_first = True
def do_POST(self):
self.rfile.read(int(self.headers.get("Content-Length", 0)))
is_first = MockLLM._first
MockLLM._first = False
if "chat/completions" in self.path:
if is_first:
resp = {"id": "c-1", "object": "chat.completion",
"choices": [{"index": 0, "finish_reason": "tool_calls", "message": {
"role": "assistant", "content": None,
"tool_calls": [{"id": "call_1", "type": "function",
"function": {"name": "view", "arguments": '{"path":"/app"}'}}]}}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}}
else:
resp = {"id": "c-2", "object": "chat.completion",
"choices": [{"index": 0, "finish_reason": "stop",
"message": {"role": "assistant", "content": "Done."}}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}}
else:
if is_first:
resp = {"id": "m-1", "type": "message", "role": "assistant",
"content": [{"type": "tool_use", "id": "toolu_1", "name": "view",
"input": {"path": "/app"}}],
"model": "mock", "stop_reason": "tool_use",
"usage": {"input_tokens": 1, "output_tokens": 1}}
else:
resp = {"id": "m-2", "type": "message", "role": "assistant",
"content": [{"type": "text", "text": "Done."}],
"model": "mock", "stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1}}
body = json.dumps(resp).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *_): pass
async def test(client, label, provider_cfg):
hook_fired = []
MockLLM._first = True
def hook(data, _inv=None):
hook_fired.append(data.get("toolName"))
return {"permissionDecision": "deny", "permissionDecisionReason": "blocked"}
session = await client.create_session({
"model": "test",
"provider": provider_cfg,
"hooks": {"on_pre_tool_use": hook},
"on_permission_request": PermissionHandler.approve_all,
})
await session.send({"prompt": "view /app"})
await asyncio.sleep(5)
await session.disconnect()
result = "PASS" if hook_fired else "FAIL"
print(f" {label:25s} {result} (hook calls: {len(hook_fired)})")
async def main():
server = HTTPServer(("127.0.0.1", PORT), MockLLM)
threading.Thread(target=server.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{PORT}"
client = CopilotClient({"use_logged_in_user": False})
await client.start()
from importlib.metadata import version
from pathlib import Path
import copilot
cli_ver = (Path(copilot.__path__[0]) / "bin" / "VERSION").read_text().strip()
sdk_ver = version("github-copilot-sdk")
print(f"Copilot CLI: {cli_ver} | SDK: {sdk_ver} | Mock: {base}\n")
await test(client, "OpenAI BYOM", {"type": "openai", "base_url": f"{base}/v1", "api_key": "x"})
await test(client, "Anthropic BYOM", {"type": "anthropic", "base_url": base, "api_key": "x"})
await client.stop()
server.shutdown()
if __name__ == "__main__":
asyncio.run(main())Output
Copilot CLI: 1.0.2 | SDK: 0.1.32 | Mock: http://127.0.0.1:18901
OpenAI BYOM PASS (hook calls: 1)
Anthropic BYOM FAIL (hook calls: 0)
Expected Behavior
preToolUse hook fires before tool execution for both providers. Both tests should show PASS.
Actual Behavior
- OpenAI BYOM: CLI sends
hooks.invokeJSON-RPC request → hook fires → tool denied. - Anthropic BYOM: CLI skips
hooks.invokeentirely → tool executes without hook interception.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
mcsruntimeRequires a change in the copilot-agent-runtime repoRequires a change in the copilot-agent-runtime reporuntime triageTriggers automated runtime triage workflowTriggers automated runtime triage workflow
Type
Fields
Give feedbackNo fields configured for issues without a type.