From f4553480552bdd264c5e2d6a4294729717d6494c Mon Sep 17 00:00:00 2001 From: Tanuj Date: Thu, 26 Feb 2026 12:29:26 -0800 Subject: [PATCH] fix: detect Droid MCP tools using ___ delimiter Droid's MCP tool naming convention uses serverName___toolName (e.g. corridor___listProjects) instead of Claude's mcp__server__tool format. Without this fix, all Droid MCP tools are classified as ExecutionTool instead of ExecutionMCP, causing the unified execution handler to silently skip them. --- unified.go | 5 ++++ unified_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/unified.go b/unified.go index 9e52395..45076bd 100644 --- a/unified.go +++ b/unified.go @@ -3,6 +3,7 @@ package hookshot import ( "encoding/json" "errors" + "strings" "github.com/CorridorSecurity/hookshot/cascade" "github.com/CorridorSecurity/hookshot/claude" @@ -337,11 +338,15 @@ func OnBeforeExecution(handler ExecutionHandler) { Register("droid-pre-tool-use", func() { RunE(func(input droid.PreToolUseInput) (droid.PreToolUseOutput, error) { // Determine execution type + // Droid MCP tools use serverName___toolName format (e.g. corridor___listProjects) + // unlike Claude's mcp__server__tool format var execType ExecutionType if input.ToolName == "Bash" { execType = ExecutionShell } else if len(input.ToolName) > 5 && input.ToolName[:5] == "mcp__" { execType = ExecutionMCP + } else if strings.Contains(input.ToolName, "___") { + execType = ExecutionMCP } else { execType = ExecutionTool } diff --git a/unified_test.go b/unified_test.go index 9ec799e..0a039d9 100644 --- a/unified_test.go +++ b/unified_test.go @@ -513,6 +513,81 @@ func TestCascadePreHooks_RunE(t *testing.T) { } } +// TestDroidPreToolUse_MCPTypeDetection verifies that droid's tool naming convention +// (serverName___toolName) is correctly classified as ExecutionMCP. +func TestDroidPreToolUse_MCPTypeDetection(t *testing.T) { + if handler := os.Getenv("HOOKSHOT_TEST_DROID_MCP_HANDLER"); handler != "" { + var capturedType ExecutionType + ClearHandlers() + OnBeforeExecution(func(ctx ExecutionContext) ExecutionDecision { + capturedType = ctx.Type + // Write the detected type to stdout so the parent can verify + os.Stdout.WriteString("type=" + string(capturedType)) + return AllowExecution() + }) + handlers[handler]() + return + } + + tests := []struct { + name string + stdinJSON string + wantType string + }{ + { + name: "Droid MCP tool with ___ delimiter", + stdinJSON: `{"session_id":"test","tool_name":"corridor___listProjects","tool_input":{},"cwd":"/tmp"}`, + wantType: "type=mcp", + }, + { + name: "Droid MCP tool with mcp__ prefix", + stdinJSON: `{"session_id":"test","tool_name":"mcp__context7__query","tool_input":{},"cwd":"/tmp"}`, + wantType: "type=mcp", + }, + { + name: "Droid built-in tool", + stdinJSON: `{"session_id":"test","tool_name":"Read","tool_input":{},"cwd":"/tmp"}`, + wantType: "type=tool", + }, + { + name: "Droid Bash tool", + stdinJSON: `{"session_id":"test","tool_name":"Bash","tool_input":{"command":"ls"},"cwd":"/tmp"}`, + wantType: "type=shell", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=^TestDroidPreToolUse_MCPTypeDetection$") + cmd.Env = append(os.Environ(), + "HOOKSHOT_TEST_DROID_MCP_HANDLER=droid-pre-tool-use", + ) + cmd.Stdin = strings.NewReader(tt.stdinJSON) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + exitCode := 0 + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } else if err != nil { + t.Fatalf("unexpected error: %v (stderr: %s)", err, stderr.String()) + } + + if exitCode != 0 { + t.Fatalf("exit code = %d, want 0 (stderr: %s)", exitCode, stderr.String()) + } + + // The handler writes the type to stdout, but the droid output JSON + // may also be written. Check that our type marker is present. + if !strings.Contains(stdout.String(), tt.wantType) { + t.Errorf("stdout = %q, want substring %q", stdout.String(), tt.wantType) + } + }) + } +} + func TestExecutionContext_ToolInput_JSON(t *testing.T) { toolInput := json.RawMessage(`{"file_path":"/test.ts"}`) ctx := ExecutionContext{