screenhand
npm
v0.5.3
Published by manushi4 — no publish provenance, so origin is unverified, but the source is public: the repository link below is self-declared yet readable, so you can inspect the code before adopting it.
Give AI eyes and hands on your desktop. ScreenHand is an open-source MCP server that lets Claude and other AI agents see your screen, click buttons, type text, and control any app on macOS and Windows.
The grade answers one question — how safe is this server for you to adopt — so it is computed in two auditable stages. Nothing below is an opinion or an LLM's guess; every line is a real term the deterministic engine applied, and the same input always yields the same number.
1. Threat score — 100 − 26.1 = 74. What the published surface and source actually contain:
| Points | What was found | Category |
|---|---|---|
| −15.4 | Assembled command execution and dynamic evaluation in the same server MTC-SRC-011 | injection |
| −9.5 | Untrusted input concatenated into a command sink ×2 MTC-SRC-009 | injection |
| −1.2 | Package runs install-time scripts MTC-SUP-010 | supply-chain |
2. Client adoption risk — 74 − 11 = 63. Three small, subtract-only factors that reflect your risk in adopting it — a clean scan proves less on a powerful, unverified or barely-inspectable package, so the grade says so plainly:
| Points | Adoption-risk factor |
|---|---|
| −10 | capability blast radius (critical) — client exposure if the model is manipulated |
| −1 | publisher verification (public source) — no provenance, but the source is public and inspectable |
Capability observations and info notes are shown under Findings but never scored.
Open any row's finding below for the file, line and evidence behind a deduction.
In the server's implementation (`dist/mcp-desktop.js:3595`): Evaluating a runtime value as code (rather than a fixed literal) executes whatever reaches it — a direct RCE primitive, and almost never necessary in legitimate code. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: // dynamic AppleScript eval (can construct blocked commands) /do\s+script/i, // Terminal.app shell execution
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/mcp-desktop.js
The implementation both builds shell commands out of runtime values (`dist/mcp-desktop.js`) and evaluates runtime values as code (`dist/mcp-desktop.js`). Each is a separate arbitrary-execution primitive; a server exposing both gives anything that reaches either one a direct path to running attacker-chosen code.
Fix: Remove the dynamic eval, and pass command arguments as an argv array instead of building a shell string. If both are genuinely required, constrain and validate every value that can reach them.
Location: server dist/mcp-desktop.js
In the server's implementation (`dist/mcp-desktop.js:3595`): Evaluating strings as code is the most direct RCE primitive; if any tool input reaches it, the server executes attacker-chosen code. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: // dynamic AppleScript eval (can construct blocked commands) /do\s+script/i, // Terminal.app shell execution
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/mcp-desktop.js
In the server's implementation (`dist/mcp-desktop.js:37`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: Sync, exec } from "node:child_process"; import { promisify } from "node:util"; const execAsync = promisify(exec); import
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/mcp-desktop.js
In the server's implementation (`dist/src/jobs/runner.js:587`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: } = await import("node:child_process"); execSync(`open ${JSON.stringify(url)}`, { timeout: 10_000 });
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/jobs/runner.js
In the server's implementation (`dist/src/native/bridge-client.js:17`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: rt { spawn } from "node:child_process"; import { EventEmitter } from "node:events"; import fs from "node:fs"; import pat
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/native/bridge-client.js
In the server's implementation (`dist/src/runtime/applescript-adapter.js:17`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: { execFile } from "node:child_process"; import { randomUUID } from "node:crypto"; import { promisify } from "node:util";
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/runtime/applescript-adapter.js
In the server's implementation (`dist/src/test-mcp-protocol.js:17`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: rt { spawn } from "node:child_process"; import { createInterface } from "node:readline"; import path from "node:path"; i
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/test-mcp-protocol.js
In the server's implementation (`dist/src/util/sanitize.js:7`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: { execSync } from "node:child_process"; import os from "node:os"; const MAX_STRING_LENGTH = 1000; const ALLOWED_URL_PROT
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/util/sanitize.js
Untrusted-input tools ([platform_learn]) co-exist with external-action tools ([applescript]). A prompt injection could cause unwanted external actions, though no direct sensitive-data leak path was found.
Evidence: untrusted [platform_learn] → sinks [applescript]
Fix: Require confirmation for state-changing/egress actions triggered after processing untrusted content.
Location: flow platform_learn → applescript
In the server's implementation (`dist/src/memory/research.js:60`): A hardcoded outbound call to a fixed external host inside server code is a classic exfiltration/telemetry channel — especially paired with reads of local data. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: const response = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/memory/research.js
In the server's implementation (`dist/src/planner/planner.js:967`): A hardcoded outbound call to a fixed external host inside server code is a classic exfiltration/telemetry channel — especially paired with reads of local data. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: const response = await fetch("https://api.anthropic.com/v1/messages", { method: "POST",
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/planner/planner.js
In the server's implementation (`dist/src/state/visual-mapper.js:124`): A hardcoded outbound call to a fixed external host inside server code is a classic exfiltration/telemetry channel — especially paired with reads of local data. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: const resp = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/state/visual-mapper.js
In the server's implementation (`dist/mcp-desktop.js:3612`): A shell/process command assembled from concatenated or interpolated values is command injection when any part is attacker-influenced — the OWASP canonical RCE flow. Verify what reaches the interpolated value. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { encoding: "utf-8",
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/mcp-desktop.js
In the server's implementation (`dist/src/jobs/runner.js:588`): A shell/process command assembled from concatenated or interpolated values is command injection when any part is attacker-influenced — the OWASP canonical RCE flow. Verify what reaches the interpolated value. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: _process"); execSync(`open ${JSON.stringify(url)}`, { timeout: 10_000 }); return { ok: true, met
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/src/jobs/runner.js
In a packaging/dev/install script (shipped, but not the server runtime) (`dist/scripts/supervisor-daemon.js:127`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: } = await import("node:child_process"); const msg = recovery.instruction.replace(/"/g, '\\"');
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server dist/scripts/supervisor-daemon.js
In a packaging/dev/install script (shipped, but not the server runtime) (`scripts/postinstall.cjs:12`): Spawning a shell/process is command-execution capability; with unsanitized tool input it is command injection / RCE. This is read from the code itself — not from the tool description — so a poisoned server cannot hide it behind honest-looking metadata.
Evidence: { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); const platform = proce
Fix: Review this call path: confirm it never receives unsanitized tool input, constrain it, or remove it. Treat a server whose code reaches these sinks as high-capability regardless of what its tools claim.
Location: server scripts/postinstall.cjs
"screenhand" executes postinstall script(s) at install time. An install hook runs at install time; most are routine build/setup, but review what it does before trusting it.
Evidence: node scripts/postinstall.cjs
Fix: Review the scripts; install with --ignore-scripts where possible and vet what they do.
Location: package screenhand
Each tool and what it can reach — statically extracted from the published source.
applescriptnetwork egressplatform_learningests untrusted inputapp_focusno sensitive capabilityapp_launchno sensitive capabilityapp_listno sensitive capabilityappsno sensitive capabilitybrowser_clickno sensitive capabilitybrowser_domno sensitive capabilitybrowser_fill_formno sensitive capabilitybrowser_human_clickno sensitive capabilitybrowser_jsno sensitive capabilitybrowser_navigateno sensitive capabilitybrowser_openno sensitive capabilitybrowser_page_infono sensitive capabilitybrowser_stealthno sensitive capabilitybrowser_tabsno sensitive capabilitybrowser_typeno sensitive capabilitybrowser_waitno sensitive capabilityclickno sensitive capabilityclick_textno sensitive capabilityclick_with_fallbackno sensitive capabilitycoverage_reportno sensitive capabilitydiscover_featuresno sensitive capabilitydragno sensitive capabilityelement_treeno sensitive capabilityexecution_planno sensitive capabilityexport_playbookno sensitive capabilityextractno sensitive capabilityfocusno sensitive capabilityingest_documentationno sensitive capabilityingest_tutorialno sensitive capabilitykeyno sensitive capabilitykey_combono sensitive capabilitylaunchno sensitive capabilitylocate_with_fallbackno sensitive capabilitymap_appno sensitive capabilitymenu_clickno sensitive capabilitynavigateno sensitive capabilityobserver_ocr_roino sensitive capabilityobserver_startno sensitive capabilityobserver_statusno sensitive capabilityobserver_stopno sensitive capabilityocrno sensitive capabilityorchestrator_startno sensitive capabilityorchestrator_statusno sensitive capabilityorchestrator_stopno sensitive capabilityorchestrator_submitno sensitive capabilityplatform_exploreno sensitive capabilityplatform_guideno sensitive capabilityplaybook_listno sensitive capabilityplaybook_preflightno sensitive capabilityplaybook_recordno sensitive capabilityplaybook_runno sensitive capabilitypressno sensitive capabilityread_with_fallbackno sensitive capabilityrecording_cancelno sensitive capabilityrecording_startno sensitive capabilityrecording_statusno sensitive capabilityrecording_stopno sensitive capabilityscan_menu_barno sensitive capabilityscreenshotno sensitive capabilityscreenshot_fileno sensitive capabilityscrollno sensitive capabilityscroll_with_fallbackno sensitive capabilityselect_with_fallbackno sensitive capabilitysession_startno sensitive capabilitytask_runno sensitive capabilitytype_intono sensitive capabilitytype_textno sensitive capabilitytype_with_fallbackno sensitive capabilityui_findno sensitive capabilityui_pressno sensitive capabilityui_set_valueno sensitive capabilityui_treeno sensitive capabilitywait_forno sensitive capabilitywait_for_stateno sensitive capabilitywatch_dialogno sensitive capabilitywatch_registerno sensitive capabilitywatch_startno sensitive capabilitywatch_statusno sensitive capabilitywatch_stopno sensitive capabilitywatch_unregisterno sensitive capabilitywindow_listno sensitive capabilitywindowsno sensitive capabilityCross-tool combinations that form a data-exfiltration primitive (untrusted input → sensitive source → external sink).
Scan history per published version. The engine is deterministic — the same version always yields the same score, so a changed score means the package itself changed.
| Version | Score | Findings | Engine | Scanned |
|---|---|---|---|---|
v0.5.3 latest |
D 63/100 | 18 | 1.13.0 | 2026-09-07 |
Show this server's live Trust Score in your README, docs or website. The badge is served straight from the registry and updates automatically after every rescan — no API key needed. It links back to this page, so anyone who sees the grade can also read the findings behind it instead of taking a number on faith.
The score above is reproducible: the same package version always yields the same result. Run it locally or over the free API — no account, no LLM, fully deterministic.
npx mcptrustchecker scan screenhand --online
Model Context Protocol (MCP) server that integrates AgentQL data extraction capabilities.
Screenshot any URL or HTML as PNG/JPEG/WebP from your AI agent. Full-page, clean, no install.
MCP server for aria51 accessibility scanner
Bridge any browser web app to Claude Code via MCP
Browserbase’s official MCP server: cloud headless browsers for agents, with sessions and screenshots.
MCP server for AI web browser automation using Browserbase and Stagehand