computer-use MCP silently returns "not_installed" for every app on native-installer CLI (NSMetadataQuery needs NSRunLoop.main)
Summary
On the native-installer build of Claude Code (Bun-compiled single binary, not npm), mcp__computer-use__request_access silently returns reason: "not_installed" for every requested app — including obviously installed Apple apps like Calculator, TextEdit, Safari, and Finder. There is no crash, no error, no telemetry event, and no user-visible log. The response looks structurally valid.
This is a distinct failure mode from the existing cluster of "Cannot read properties of undefined (reading 'registerEscape' / 'checkAccessibility')" issues (#41118, #41190, #41278, #41355, #41404, #41538, #41606, #41807, #42263, #42380, #45923). Those all describe the npm install path where the computer-use-swift.node addon is missing and vS() is undefined. On the native installer, the addon IS embedded at /$bunfs/root/computer-use-swift.node and loads successfully — the failure happens one layer deeper.
Environment
- Claude Code 2.1.104 (native installer, not npm), binary at
~/.local/share/claude/versions/2.1.104(Mach-O arm64) - macOS 15 (Darwin 24.6.0), Apple Silicon
- Pro/Max plan, computer-use feature flag enabled (tool calls route through
getComputerUseMCPToolOverrides/H21/eP1)
Steps to reproduce
- Confirm you're on the native installer (
file $(which claude)→Mach-O 64-bit executable, not a JS shebang). - Invoke
mcp__computer-use__list_granted_applications— works, returns{"allowedApps":[],"grantFlags":{...}}. - Invoke
mcp__computer-use__cursor_position— works, returns real coordinates. - Invoke
mcp__computer-use__request_accesswith any mix of real apps, e.g.{"apps": ["Calculator", "TextEdit", "Safari", "Terminal", "com.apple.finder"], "reason": "..."}.
Actual result:
{
"granted": [],
"denied": [
{"bundleId": "Calculator", "reason": "not_installed"},
{"bundleId": "TextEdit", "reason": "not_installed"},
{"bundleId": "Safari", "reason": "not_installed"},
{"bundleId": "Terminal", "reason": "not_installed"},
{"bundleId": "com.apple.finder", "reason": "not_installed"}
],
"screenshotFiltering": "native"
}
Note that bundleId in each denied entry is just the echoed input string (literally "Calculator", not com.apple.Calculator), because H.resolved is undefined and oP1 falls back to H.requestedName.
Expected result: either the permission dialog appears with the requested apps resolved to their real bundle IDs, or a clear error indicates why the installed-apps enumeration failed.
Root cause (from bytecode analysis)
The JS resolver b91 builds lookup maps from whatever H.executor.listInstalledApps() returns, then maps each request to {requestedName, resolved: map.get(...)}. When resolved is undefined, the dialog serializer oP1 emits:
function oP1(H) {
return {
bundleId: H.resolved?.bundleId ?? H.requestedName,
reason: H.resolved ? "user_denied" : "not_installed"
};
}
So "not_installed" is not an affirmative "this app is absent" signal — it's just "the resolver found nothing." For every requested app to hit this path, listInstalledApps() must be returning an empty array (or a list that doesn't contain Calculator, TextEdit, Safari, Terminal, or Finder — which is effectively impossible on any real Mac).
listInstalledApps() in UF6 is:
async listInstalledApps() { return sp(() => _.apps.listInstalled()) }
where _ = vS() is the native computer-use-swift.node module. From the binary's Swift symbol table:
chicagoListInstalled
performSpotlightQuery()
kMDItemContentType == %@ → com.apple.application-bundle
kMDItemCFBundleIdentifier
NSMetadataQuery failed to start (Spotlight may be indexing or disabled)
v16@?0@"NSNotification"8 ← callback taking an NSNotification
So the Swift implementation enumerates installed apps by starting an NSMetadataQuery for kMDItemContentType == "com.apple.application-bundle" and listening for NSMetadataQueryDidFinishGatheringNotification.
NSMetadataQuery requires an active NSRunLoop.main on the AppKit main thread to deliver its completion notification (it's a notification-based async Cocoa API; see Apple's NSMetadataQuery docs). The CLI binary doesn't call NSApplicationMain and has no AppKit main run loop, so the query either (a) returns false from start() and hits the "NSMetadataQuery failed to start (Spotlight may be indexing or disabled)" error path, or (b) starts but its delegate never fires.
The JS side has a drainMainRunLoop pump:
function A91(H) { H._drainMainRunLoop() }
tsH = setInterval(A91, 1, vS()) // pump every 1ms from a Bun worker
but pumping _drainMainRunLoop() via setInterval from a Bun worker is not equivalent to NSApplicationMain's main thread run loop. Notification-based Cocoa APIs won't deliver their callbacks through a side loop.
Why other tools still work
cursor_positionworks becauseGI().mouseLocation()goes through a different native addon (computer-use-input.node, the Rust crate) and usesCGEventSourceread APIs that don't need a main run loop or TCC entitlements.list_granted_applicationsworks because it's pure JS state (just returns the allowlist).request_accessfails silently because its gating dependency (listInstalled()) depends on a Cocoa async API that silently produces nothing in the CLI runtime.- By extension, screenshots via
SCShareableContentand theCGEventTapCreateEsc hotkey would also fail — the binary contains their error strings (Missing screen recording permission,[cu-esc] CGEvent.tapCreate failed — check Accessibility permission) but those are at least user-visible, unlike this silentnot_installedcase.
Design signal
The binary already carries two bundle identities that signal this was designed for the desktop app, not the CLI:
- JS:
IS6 = "com.anthropic.claude-code.cli-no-window"— literal "cli-no-window" sentinel host bundle ID used whenTQq()can't detect the hosting terminal. - Swift:
com.anthropic.claudefordesktop— the intended host bundle ID (Claude for Desktop).
There's also an explicit "CLI variant" factory, createComputerUseMcpServerForCli (oh9), that creates a server via xF6(H, _) with only two arguments, which takes the stub branch of xF6:
return T.setRequestHandler(lg, async (A) => {
O.warn(`[${K}] tool call "${A.params.name}" reached the stub handler — no session context bound...`);
return {
content: [{
type: "text",
text: "This computer-use server instance is not wired to a session. Per-session app permissions are not available on this code path."
}],
isError: true
};
}), T;
The only reason tools appear to work at all from the interactive CLI is that getComputerUseMCPToolOverrides (H21) replaces the call function for every tool with one that dispatches through eP1() → PC_(oeH(), MyH(), Uh9()), bypassing the stub's CallTool handler. So the JS/dispatcher layer is fine; the problem is specifically in the native listInstalled() Cocoa path.
Why this is report-worthy even though it's "just" running in the wrong context
- It's silent. A user sees "Calculator not_installed" on a machine where Calculator is obviously installed, and there's no indication that this is a context issue vs. a real enumeration failure. The existing
Cannot read properties of undefinedcrash is ugly but at least actionable. - It's the next layer users hit after the #41190 workaround. The canonical fix for the crude crash is "use the native installer." Users who follow that advice don't crash — they land here, with
request_accesssilently denying everything. - It's not documentable as a TCC permission issue. #41099 describes the genuine permission dead-end where Claude surfaces
"Accessibility and Screen Recording permission(s) not yet granted"with no binary path. Once that user granted perms to the CLI binary, their computer-use worked. This failure mode survives full permission grants —NSMetadataQuerydoesn't need TCC, it needs an NSRunLoop.
Suggested fixes (pick any)
- Short term / docs: Detect CLI runtime (
process.env.__CFBundleIdentifieris unset and host bundle resolves to thecli-no-windowsentinel) and emit a clear error fromlistInstalled(): "computer-use enumeration requires Claude for Desktop; see docs." Route the computer-use MCP through the desktop app's helper when the desktop app is installed. - Medium term: Replace
NSMetadataQuerywith a synchronous enumeration that doesn't depend on the main run loop — e.g., walk/Applicationsand/System/Applications+~/Applications, read eachContents/Info.plistwithCFPropertyListCreateFromXMLData, extractCFBundleIdentifier/CFBundleName. Slower but CLI-safe. - Long term: Spin up a minimal
NSApplicationin the Swift addon's init path (NSApplication.shared,activationPolicy = .accessory, start anNSRunLoopon a dedicated thread) so Cocoa notification-based APIs work from a non-.apphost. This is the same pattern Electron / CEF use for headless AppKit hosting.
Related
- #41355 —
@ant/computer-use-swiftmissing from npm (different install path, same subsystem) - #41099 — request_access permission dead-end in CLI
- #41118, #41190, #41278, #41404, #41538, #41606, #41807, #42263, #42380, #45923 —
registerEscape/checkAccessibilityundefined crash cluster - #41209 —
computer-use-input.nodemissing on npm darwin-arm64 - #38471 — original feature request for CLI computer-use
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗