Skip to content

feat(cua-driver): add Claude Code computer-use compatibility mode - #1424

Merged
f-trycua merged 6 commits into
mainfrom
feat/cua-driver-claude-code-compat
May 2, 2026
Merged

feat(cua-driver): add Claude Code computer-use compatibility mode#1424
f-trycua merged 6 commits into
mainfrom
feat/cua-driver-claude-code-compat

Conversation

@f-trycua

@f-trycua f-trycua commented May 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add a Claude Code compatibility MCP mode that keeps normal CuaDriver tools and replaces only screenshot with a window-scoped shim
  • make raw screenshot require window_id so it no longer captures the full desktop by default
  • update Claude Code skill, docs, and install hints for the compatibility registration path
  • expand uninstall cleanup to remove CuaDriver Claude MCP registrations and daemon cache state

Verification

  • swift build
  • swift test
  • bash -n scripts/install.sh
  • bash -n scripts/install-local.sh
  • bash -n scripts/uninstall.sh
  • git diff --check
  • staged diff audit for secrets/local paths/reverse-engineering wording

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Claude Code computer-use compatibility mode with --claude-code-computer-use-compat flag for MCP registration
    • Updated screenshot tool to require window identifier for window-scoped captures
    • Added configuration command support for capture mode and image dimension settings
  • Documentation

    • Added comprehensive guides for Claude Code compatibility mode registration and usage across all documentation

@vercel

vercel Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 2, 2026 4:03am

Request Review

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03ef7166-e45e-4a82-8041-a11d77a4db7d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces Claude Code computer-use compatibility mode for cua-driver. It adds a new --claude-code-computer-use-compat flag to CLI commands, implements a window-scoped screenshot tool shim, updates the existing screenshot tool to require window_id, modifies installation scripts for setup guidance, and documents the feature across guide and reference documentation.

Changes

Claude Code Computer-Use Compatibility Feature

Layer / File(s) Summary
Tool Interface Changes
libs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swift, libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift, libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift
ScreenshotTool now requires window_id (removes optional full-display capture). MouseInput.click() adds useFrontmostHIDPath parameter. CuaDriverMCPServer.make() accepts dynamic serverName parameter.
Compatibility Tool Implementation
libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift
New ClaudeCodeComputerUseCompatTools provides a window-only screenshot shim that accepts pid and window_id, captures a single window as JPEG, maintains session state via async actor, and returns base64-encoded image plus descriptive text. Registry .claudeCodeComputerUseCompat combines native tools with the shim.
CLI Flag Support
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift, libs/cua-driver/Sources/CuaDriverCLI/ConfigCommand.swift, libs/cua-driver/Sources/CuaDriverCLI/CallCommand.swift, libs/cua-driver/Sources/CuaDriverCLI/Docs/CLIDocExtractor.swift
MCPCommand and MCPConfigCommand add --claude-code-computer-use-compat flag. When enabled, uses computer-use server name and .claudeCodeComputerUseCompat registry instead of defaults. CallCommand conditionally bootstraps AppKit activation policy for AppKit-backed tools before in-process invocation. ConfigCommand adds capture_mode and max_image_dimension config key support. CLIDocExtractor documents new flag.
MCP Server Wiring
libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift
Server instantiation uses dynamic serverName parameter to emit either "cua-driver" (default) or "computer-use" (compat mode).
Installation & Setup
libs/cua-driver/scripts/install.sh, libs/cua-driver/scripts/install-local.sh, libs/cua-driver/scripts/uninstall.sh
Install scripts add Claude Code compatibility setup instructions. Uninstall script removes cache directory (~/Library/Caches/cua-driver), scrubs Claude MCP registrations from ~/.claude.json via embedded Python3 script, and runs claude mcp remove across scopes when CLI is available.
Documentation
docs/content/docs/cua-driver/guide/getting-started/installation.mdx, docs/content/docs/cua-driver/guide/getting-started/integrations.mdx, docs/content/docs/cua-driver/reference/cli-reference.mdx, docs/content/docs/cua-driver/reference/mcp-tools.mdx, libs/cua-driver/README.md, libs/cua-driver/Skills/cua-driver/README.md, libs/cua-driver/Skills/cua-driver/SKILL.md
Guides and reference docs explain the compatibility mode: registration commands, tool behavior changes (window-only capture with pid/window_id requirement, window-local coordinate frame), and clarifications that this is a CuaDriver MCP compatibility shim, not Anthropic's native computer-use API.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as cua-driver CLI
    participant Reg as ToolRegistry
    participant Server as MCP Server
    participant Tool as Screenshot Tool
    participant AppKit

    User->>CLI: mcp --claude-code-computer-use-compat
    CLI->>CLI: Parse --claude-code-computer-use-compat flag
    
    alt Compat Mode Enabled
        CLI->>Reg: Get .claudeCodeComputerUseCompat registry
        CLI->>Server: Create with serverName="computer-use"<br/>and compat registry
    else Default Mode
        CLI->>Reg: Get .default registry
        CLI->>Server: Create with serverName="cua-driver"<br/>and default registry
    end
    
    Server-->>CLI: MCP Server initialized
    
    User->>Server: Call screenshot(pid, window_id)
    
    alt Compat Mode
        Server->>Tool: CompatTools.screenshot invoked
        Tool->>AppKit: Query visible windows for pid/window_id
        Tool->>AppKit: Capture single window as JPEG
        Tool-->>Server: Return window-local image + metadata
    else Default Mode
        Server->>Tool: ScreenshotTool invoked
        Tool->>AppKit: Capture specified window_id
        Tool-->>Server: Return window screenshot
    end
    
    Server-->>User: Image result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ddupont808

🐰 A shiny window appears, caught by our lens,
With pid and ID, the frame now makes sense,
No fullscreen glare, just the app's own display,
Claude Code plays nice—compatibility's way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Claude Code computer-use compatibility mode to cua-driver, which is the primary objective reflected across all changed files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cua-driver-claude-code-compat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift (1)

96-110: ⚡ Quick win

Document useFrontmostHIDPath behavior in the API comment.

The doc block still reads like HID posting is unconditional. With Line 110 + Line 127, HID is conditional and caller-controlled; this should be explicit to prevent incorrect assumptions about cursor movement.

Proposed doc update
-    /// (`control`, `fn`); unknown names are ignored. Events are
-    /// posted via auth-signed `SLEventPostToPid` AND the public HID
-    /// tap — see the file-level doc for the rationale.
+    /// (`control`, `fn`); unknown names are ignored.
+    /// By default, frontmost targets route through `.cghidEventTap`
+    /// (`useFrontmostHIDPath = true`), while non-frontmost targets
+    /// use pid-routed delivery (`SLEventPostToPid`/`CGEvent.postToPid`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift` around lines 96
- 110, Update the API doc for
MouseInput.click(at:toPid:button:count:modifiers:useFrontmostHIDPath:) to
explicitly state that HID posting is conditional and controlled by the
useFrontmostHIDPath parameter: when true the function will also post events via
the frontmost HID path (which can move the global cursor), when false it will
only use the auth-signed SLEventPostToPid delivery (which targets the target PID
without changing the system cursor); mention the default (true) and the
implications for callers who rely on cursor movement or not.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libs/cua-driver/scripts/uninstall.sh`:
- Around line 148-149: The uninstall logic only removes registrations named
"cua-driver" via the should_remove function; update should_remove (and any other
cleanup checks referring to the registration name) to also accept
"cua-computer-use" so compat MCP entries get cleaned up — e.g., change the
predicate in should_remove (and the matching logic used in the cleanup loop that
also appears around the 210-215 area) to return true when name == "cua-driver"
OR name == "cua-computer-use" or when invokes_cua_driver(server) is true.

In `@libs/cua-driver/Skills/cua-driver/SKILL.md`:
- Around line 155-156: Replace the shorthand "compat" with the full word
"compatibility" in the user-facing documentation paragraph that describes
CuaDriver tools and the `screenshot` behavior (the sentence starting "normal
CuaDriver tools and changes only `screenshot`..."); update any nearby
occurrences in SKILL.md referencing `screenshot`'s compatibility mode so the
term is consistently "compatibility" throughout the user-facing text.

In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift`:
- Around line 72-74: The printed install commands for Claude/Codex interpolate
the resolvedBinaryPath() into a shell command unquoted, which breaks if the path
contains spaces; update the two print statements in the switch cases (the
"claude" and "codex" branches that currently print the command) to quote the
executable path when interpolating (e.g., wrap the binary variable in double
quotes or properly shell-escape it) so the generated command uses
"...\(binary)..." instead of an unquoted path in both the Claude and Codex print
lines.

In
`@libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift`:
- Around line 11-23: The compat session only replaces the screenshot handler but
leaves coordinate-taking tools bound to the global frame, so the downstream tool
coordinates never update; update the compat registration to also replace or wrap
all coordinate-dependent tools from ToolRegistry.default (e.g.,
click/move/region tools) so they query
ClaudeCodeComputerUseCompatSession.shared.currentActiveWindow() (use
setActiveWindow/currentActiveWindow) to compute window-local frames; ensure the
registry swap applied at the same places called out (around the other
occurrences at lines referenced) so every tool that computes coordinates uses
the activeWindow context instead of the global frame.
- Around line 64-80: The code currently force-casts numeric values with
Int32(pid) and UInt32(context.window.id) which can trap on out-of-range values;
change to checked conversions using init?(exactly:) before calling
compatWindowContext and capture.captureWindow: e.g., first convert the extracted
pid and window_id into fixed-width types with guard let pid32 = Int32(exactly:
pid) and guard let winID32 = Int32(exactly: windowID) (or the appropriate
signedness expected by compatWindowContext), return errorResult(...) on failure,
use pid32 when calling compatWindowContext(forPid:), and when passing
context.window.id into capture.captureWindow convert with guard let uiWinID =
UInt32(exactly: context.window.id) and return error on failure; update
references to use these checked variables (compatWindowContext,
capture.captureWindow, pid, windowID, context.window.id).

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swift`:
- Around line 55-73: The code currently force-casts windowID with
UInt32(windowID) which will trap on negative or overflow values; update the
validation to perform a checked conversion using UInt32(exactly: windowID) (or
UInt32(exactly: rawWindowID) if you rename) and if that returns nil return a
CallTool.Result with isError: true and a clear message about invalid window_id
range instead of calling capture.captureWindow with an unchecked value; ensure
you update the guard that extracts arguments?["window_id"]?.intValue to use this
checked UInt32 conversion before invoking capture.captureWindow.

---

Nitpick comments:
In `@libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift`:
- Around line 96-110: Update the API doc for
MouseInput.click(at:toPid:button:count:modifiers:useFrontmostHIDPath:) to
explicitly state that HID posting is conditional and controlled by the
useFrontmostHIDPath parameter: when true the function will also post events via
the frontmost HID path (which can move the global cursor), when false it will
only use the auth-signed SLEventPostToPid delivery (which targets the target PID
without changing the system cursor); mention the default (true) and the
implications for callers who rely on cursor movement or not.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 768b22dd-2da9-4f9d-8a7a-3626bd6fbf1d

📥 Commits

Reviewing files that changed from the base of the PR and between e4b047a and 3190059.

📒 Files selected for processing (18)
  • docs/content/docs/cua-driver/guide/getting-started/installation.mdx
  • docs/content/docs/cua-driver/guide/getting-started/integrations.mdx
  • docs/content/docs/cua-driver/reference/cli-reference.mdx
  • docs/content/docs/cua-driver/reference/mcp-tools.mdx
  • libs/cua-driver/README.md
  • libs/cua-driver/Skills/cua-driver/README.md
  • libs/cua-driver/Skills/cua-driver/SKILL.md
  • libs/cua-driver/Sources/CuaDriverCLI/CallCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/ConfigCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/Docs/CLIDocExtractor.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift
  • libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift
  • libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swift
  • libs/cua-driver/scripts/install-local.sh
  • libs/cua-driver/scripts/install.sh
  • libs/cua-driver/scripts/uninstall.sh

Comment thread libs/cua-driver/scripts/uninstall.sh Outdated
Comment on lines +148 to +149
def should_remove(name, server):
return name == "cua-driver" or invokes_cua_driver(server)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Also remove the cua-computer-use Claude registration.

The compat install path registers under cua-computer-use, but both cleanup paths only target cua-driver by name. That leaves compat MCP entries behind after uninstall for the exact flow this PR adds.

Proposed fix
-def should_remove(name, server):
-    return name == "cua-driver" or invokes_cua_driver(server)
+def should_remove(name, server):
+    return name in {"cua-driver", "cua-computer-use"} or invokes_cua_driver(server)
-if command -v claude >/dev/null 2>&1; then
-    for SCOPE in local project user; do
-        if claude mcp remove cua-driver -s "$SCOPE" >/dev/null 2>&1; then
-            log "removed Claude MCP server cua-driver from $SCOPE scope"
-        fi
-    done
+if command -v claude >/dev/null 2>&1; then
+    for SERVER in cua-driver cua-computer-use; do
+        for SCOPE in local project user; do
+            if claude mcp remove "$SERVER" -s "$SCOPE" >/dev/null 2>&1; then
+                log "removed Claude MCP server $SERVER from $SCOPE scope"
+            fi
+        done
+    done
 else
     log "claude CLI not found (skipping Claude MCP CLI cleanup)"
 fi

Also applies to: 210-215

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/scripts/uninstall.sh` around lines 148 - 149, The uninstall
logic only removes registrations named "cua-driver" via the should_remove
function; update should_remove (and any other cleanup checks referring to the
registration name) to also accept "cua-computer-use" so compat MCP entries get
cleaned up — e.g., change the predicate in should_remove (and the matching logic
used in the cleanup loop that also appears around the 210-215 area) to return
true when name == "cua-driver" OR name == "cua-computer-use" or when
invokes_cua_driver(server) is true.

Comment on lines +155 to +156
normal CuaDriver tools and changes only `screenshot`. The compat
`screenshot` requires `pid` and `window_id`, captures only that target

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use “compatibility” instead of “compat” in user-facing docs.

Line 155 uses shorthand (compat) in a prominent explanatory paragraph; expanding it improves consistency with the rest of the docs.

✏️ Suggested wording tweak
- Observation: Claude Code vision flows appear to treat a screenshot MCP
- tool as the image-grounding anchor. This compatibility mode keeps the
- normal CuaDriver tools and changes only `screenshot`. The compat
+ Observation: Claude Code vision flows appear to treat a screenshot MCP
+ tool as the image-grounding anchor. This compatibility mode keeps the
+ normal CuaDriver tools and changes only `screenshot`. The compatibility
  `screenshot` requires `pid` and `window_id`, captures only that target
  window, and returns the window-local pixel coordinate frame.
🧰 Tools
🪛 LanguageTool

[grammar] ~155-~155: Ensure spelling is correct
Context: ...ools and changes only screenshot. The compat screenshot requires pid and `window...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Skills/cua-driver/SKILL.md` around lines 155 - 156, Replace
the shorthand "compat" with the full word "compatibility" in the user-facing
documentation paragraph that describes CuaDriver tools and the `screenshot`
behavior (the sentence starting "normal CuaDriver tools and changes only
`screenshot`..."); update any nearby occurrences in SKILL.md referencing
`screenshot`'s compatibility mode so the term is consistently "compatibility"
throughout the user-facing text.

Comment on lines +72 to +74
print("claude mcp add --transport stdio \(serverName) -- \(binary) \(commandArgs)")
case "codex":
print("codex mcp add cua-driver -- \(binary) mcp")
print("codex mcp add \(serverName) -- \(binary) \(commandArgs)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Quote the executable path in the generated Claude/Codex install commands.

resolvedBinaryPath() can legitimately contain spaces, and these two snippets interpolate it into a shell command unquoted. In that case the pasted command is tokenized incorrectly and MCP registration fails.

Suggested fix
     func run() throws {
         let binary = resolvedBinaryPath()
+        let shellBinary = shellEscape(binary)
         // Observed Claude Code behavior: the exact config key "computer-use"
         // is reserved, so external stdio registrations use a distinct key.
         let serverName = claudeCodeComputerUseCompat ? "cua-computer-use" : "cua-driver"
@@
         case "claude":
-            print("claude mcp add --transport stdio \(serverName) -- \(binary) \(commandArgs)")
+            print("claude mcp add --transport stdio \(serverName) -- \(shellBinary) \(commandArgs)")
         case "codex":
-            print("codex mcp add \(serverName) -- \(binary) \(commandArgs)")
+            print("codex mcp add \(serverName) -- \(shellBinary) \(commandArgs)")
@@
     private func resolvedBinaryPath() -> String {
         // `Bundle.main.executablePath` points at the physical binary
         // inside the .app bundle even when invoked via a symlink. Falls
         // back to argv[0] for raw `swift run` contexts.
         if let path = Bundle.main.executablePath {
             return path
         }
         return CommandLine.arguments.first ?? "cua-driver"
     }
+
+    private func shellEscape(_ value: String) -> String {
+        "'\(value.replacingOccurrences(of: "'", with: "'\"'\"'"))'"
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift` around lines 72
- 74, The printed install commands for Claude/Codex interpolate the
resolvedBinaryPath() into a shell command unquoted, which breaks if the path
contains spaces; update the two print statements in the switch cases (the
"claude" and "codex" branches that currently print the command) to quote the
executable path when interpolating (e.g., wrap the binary variable in double
quotes or properly shell-escape it) so the generated command uses
"...\(binary)..." instead of an unquoted path in both the Claude and Codex print
lines.

Comment on lines +11 to +23
private actor ClaudeCodeComputerUseCompatSession {
static let shared = ClaudeCodeComputerUseCompatSession()

private var activeWindow: CompatWindowContext?

func setActiveWindow(_ context: CompatWindowContext?) {
activeWindow = context
}

func currentActiveWindow() -> CompatWindowContext? {
activeWindow
}
}

@coderabbitai coderabbitai Bot May 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The compat screenshot never changes downstream tool coordinates.

activeWindow is only written here, and this registry swaps in only the screenshot handler. Since the coordinate-taking tools still come from ToolRegistry.default, this mode does not actually establish the window-local frame promised by the tool description.

Also applies to: 83-88, 112-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift`
around lines 11 - 23, The compat session only replaces the screenshot handler
but leaves coordinate-taking tools bound to the global frame, so the downstream
tool coordinates never update; update the compat registration to also replace or
wrap all coordinate-dependent tools from ToolRegistry.default (e.g.,
click/move/region tools) so they query
ClaudeCodeComputerUseCompatSession.shared.currentActiveWindow() (use
setActiveWindow/currentActiveWindow) to compute window-local frames; ensure the
registry swap applied at the same places called out (around the other
occurrences at lines referenced) so every tool that computes coordinates uses
the activeWindow context instead of the global frame.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the current tool implementations. I did not wrap the coordinate-taking tools because the normal CuaDriver pixel tools already accept window-local screenshot coordinates and convert through WindowCoordinateSpace using pid plus window_id when supplied. In compat mode the screenshot shim requires pid and window_id, and the docs now tell Claude Code users to pass that same target window into the normal CuaDriver tools. Wrapping every coordinate tool here would duplicate the existing coordinate conversion layer and broaden the compat mode beyond the intended screenshot-only shim.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check WindowCoordinateSpace and how coordinate tools use pid + window_id
rg -n "WindowCoordinateSpace" --type swift -C3
echo "---"
rg -n "window_id" libs/cua-driver/Sources/CuaDriverServer/Tools/ -C3
echo "---"
rg -n "pid" libs/cua-driver/Sources/CuaDriverServer/Tools/ -C3 --type swift

Length of output: 312101


@coderabbit-user Thanks for the clarification — I re-checked the code paths and your reasoning lines up with what’s implemented.

What I verified

  • Pixel tools already convert window-local pixels using pid + window_id:
    • click: WindowCoordinateSpace.screenPoint(fromImagePixel:..., forPid:, windowId:) when window_id is present; otherwise pid-only fallback to the pid’s frontmost window.
    • double_click/right_click/drag: same pattern — windowId-aware mapping when provided; pid-only frontmost fallback when omitted.
  • Element-indexed paths already require window_id and are scoped per (pid, window_id).
  • Image resizing coordination: GetWindowStateTool sets ImageResizeRegistry’s ratio for the pid when it resizes the screenshot, and click/drag use that recorded ratio to scale x,y back up.

Net: If Claude Code calls the normal click/move/drag tools with the same pid + window_id used for the compat screenshot, the mapping lands in the correct window-local frame without additional wrappers. So I’ll withdraw the earlier “wrap everything” suggestion.

One small follow-up to consider

  • If the compat screenshot is ever downscaled (e.g., to max_image_dimension) and only records scaleFactor in the private ClaudeCodeComputerUseCompatSession, pixel tools won’t see it because they read ImageResizeRegistry.ratio(forPid:). Two easy options:
    • Emit screenshots at native scale in compat mode; or
    • In the compat screenshot handler, also call ImageResizeRegistry.shared.setRatio(scaleFactor, forPid: pid) so existing click/drag scaling logic continues to work without extra wrappers.

If you’re already returning native-scale JPEGs in compat mode, then no change is needed. Otherwise, the single-line ImageResizeRegistry update in the compat screenshot path would close that gap and make the session actor unnecessary. If you’d like, I can draft that tiny patch.

Comment on lines +64 to +80
guard let pid = arguments?["pid"]?.intValue else {
return errorResult("Missing required integer field `pid`.")
}
guard let windowID = arguments?["window_id"]?.intValue else {
return errorResult("Missing required integer field `window_id`.")
}
guard let context = compatWindowContext(
forPid: Int32(pid),
windowID: windowID
) else {
return errorResult(
"No visible layer-0 window \(windowID) found for pid \(pid). Use `list_windows` to choose an on-screen target window."
)
}
let shot = try await capture.captureWindow(
windowID: UInt32(context.window.id),
format: .jpeg,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Swift, do Int32(someInt)andUInt32(someInt)trap whensomeInt is outside the destination type's range? What checked conversion API is recommended for request validation?

💡 Result:

Yes, in Swift, Int32(someInt) and UInt32(someInt) trap (crash at runtime) when someInt is outside the destination type's representable range. This is the range-checked behavior of the default init(:) initializer provided by the BinaryInteger protocol, which all fixed-width integer types like Int32 and UInt32 conform to. Apple documentation explicitly states that using init(:) triggers a runtime error if the value cannot be represented exactly in the destination type. For request validation, where you need to safely handle potentially out-of-range values (e.g., from untrusted input like JSON or HTTP parameters), the recommended checked conversion API is the failable initializer init?(exactly:). It returns nil if the value is out of range, allowing you to gracefully handle invalid input without trapping. Other options like init(clamping:) or init(truncatingIfNeeded:) exist for specific behaviors, but init?(exactly:) is preferred for validation as it precisely checks representability without modifying the value.

Citations:


🏁 Script executed:

# Find the file and check if it exists
fd "ClaudeCodeComputerUseCompatTools.swift"

Repository: trycua/cua

Length of output: 133


🏁 Script executed:

# Read the specific lines mentioned in the review
if [ -f "libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift" ]; then
  sed -n '60,85p' "libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift"
fi

Repository: trycua/cua

Length of output: 1230


🏁 Script executed:

# Check broader context around those lines to understand the function signature
if [ -f "libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift" ]; then
  sed -n '40,95p' "libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift"
fi

Repository: trycua/cua

Length of output: 2685


🏁 Script executed:

# Find the definition of intValue property (likely a JSON value extension)
rg "intValue" libs/cua-driver/Sources/CuaDriverServer/ -A 2 -B 2

Repository: trycua/cua

Length of output: 24289


🏁 Script executed:

# Find the compatWindowContext function signature
rg "func compatWindowContext" libs/cua-driver/Sources/CuaDriverServer/ -A 5

Repository: trycua/cua

Length of output: 712


🏁 Script executed:

# Check the type of context.window.id by looking at the window/context definition
rg "struct.*Window|class.*Window" libs/cua-driver/Sources/CuaDriverServer/ -A 10 | head -50

Repository: trycua/cua

Length of output: 2313


Guard pid and window_id before fixed-width casts.

Int32(pid) and UInt32(context.window.id) trap on out-of-range input, turning a malformed MCP request into a process abort instead of a normal tool error. Use init?(exactly:) checked conversions, which is already the pattern used throughout the codebase (ScrollTool, TypeTextTool, PageTool, ListWindowsTool).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/cua-driver/Sources/CuaDriverServer/ClaudeCodeComputerUseCompatTools.swift`
around lines 64 - 80, The code currently force-casts numeric values with
Int32(pid) and UInt32(context.window.id) which can trap on out-of-range values;
change to checked conversions using init?(exactly:) before calling
compatWindowContext and capture.captureWindow: e.g., first convert the extracted
pid and window_id into fixed-width types with guard let pid32 = Int32(exactly:
pid) and guard let winID32 = Int32(exactly: windowID) (or the appropriate
signedness expected by compatWindowContext), return errorResult(...) on failure,
use pid32 when calling compatWindowContext(forPid:), and when passing
context.window.id into capture.captureWindow convert with guard let uiWinID =
UInt32(exactly: context.window.id) and return error on failure; update
references to use these checked variables (compatWindowContext,
capture.captureWindow, pid, windowID, context.window.id).

Comment on lines +55 to +73
guard let windowID = arguments?["window_id"]?.intValue else {
return CallTool.Result(
content: [
.text(
text: "Missing required `window_id`. Use `list_windows` first, then call `screenshot` for one window.",
annotations: nil,
_meta: nil
)
],
isError: true
)
}

do {
let shot: Screenshot
if let windowID {
shot = try await capture.captureWindow(
windowID: UInt32(windowID),
format: format,
quality: quality
)
} else {
shot = try await capture.captureMainDisplay(
format: format,
quality: quality
)
}
let shot = try await capture.captureWindow(
windowID: UInt32(windowID),
format: format,
quality: quality
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Swift, does UInt32(someInt)trap whensomeIntis negative or outsideUInt32's range? What checked conversion API should be used instead?

💡 Result:

Yes, UInt32(someInt) traps (causes a runtime error) if someInt is negative or outside UInt32's representable range (0...UInt32.max). Use the failable initializer UInt32(exactly: someInt) for checked conversion, which returns nil if the value cannot be exactly represented. Alternatively, UInt32(truncatingIfNeeded: someInt) truncates without trapping, or UInt32(clamping: someInt) clamps to the range.

Citations:


Validate window_id range before casting to prevent server crash.

UInt32(windowID) traps if the value is negative or exceeds UInt32.max, causing a runtime crash instead of returning isError: true. Use UInt32(exactly: rawWindowID) for a checked conversion that returns nil on invalid range, allowing graceful error handling.

Proposed fix
-            guard let windowID = arguments?["window_id"]?.intValue else {
+            guard
+                let rawWindowID = arguments?["window_id"]?.intValue,
+                let windowID = UInt32(exactly: rawWindowID)
+            else {
                 return CallTool.Result(
                     content: [
                         .text(
-                            text: "Missing required `window_id`. Use `list_windows` first, then call `screenshot` for one window.",
+                            text: "Missing or invalid `window_id`. Use `list_windows` first, then pass a valid window id.",
                             annotations: nil,
                             _meta: nil
                         )
                     ],
                     isError: true
                 )
             }
@@
-                let shot = try await capture.captureWindow(
-                    windowID: UInt32(windowID),
+                let shot = try await capture.captureWindow(
+                    windowID: windowID,
                     format: format,
                     quality: quality
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swift` around
lines 55 - 73, The code currently force-casts windowID with UInt32(windowID)
which will trap on negative or overflow values; update the validation to perform
a checked conversion using UInt32(exactly: windowID) (or UInt32(exactly:
rawWindowID) if you rename) and if that returns nil return a CallTool.Result
with isError: true and a clear message about invalid window_id range instead of
calling capture.captureWindow with an unchecked value; ensure you update the
guard that extracts arguments?["window_id"]?.intValue to use this checked UInt32
conversion before invoking capture.captureWindow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant