2.10.0 note: This document covers the runtime security posture (network behavior, token handling, policy engine, consent ledger, supply-chain hardening since 2.2.0). For the per-finding response to the ClawHub Security Scan of v2.6.1, see
SECURITY_SCAN_NOTES.md; for the one-screen operator summary (risk item / current default / how to tighten / config switch / verify) see the Security Posture table inREADME.md. A local reproduction of the ClawHub static heuristic is available vianpm run scan:security(zero unexpected findings on 2.6.3 — the 2.6.3 changes are install-time UX only and do not touch the runtime callsites tracked by the scan).
Use GitHub's private vulnerability reporting flow from the repository's Security tab. Include the affected component and version or commit, reproduction steps, expected and observed impact, and any known mitigation.
Do not open a public issue for a suspected vulnerability or disclose it before a fix or coordinated disclosure plan is available. Routine dependency updates and non-sensitive bugs can use normal GitHub issues.
JS Eyes is a local-first browser automation stack. Its normal runtime loop talks only to the JS Eyes server you configure, which defaults to localhost:18080.
There are two deployment shapes to keep in mind:
- ClawHub / bundle deployment: install the JS Eyes bundle, run
npm installin the bundle root, registeropenclaw-plugin, and allow the plugin tools in OpenClaw. - Source-repo / development deployment: clone this repository, run
npm installin the repo root, point OpenClaw at the repo-rootopenclaw-plugin, and optionally load the unpacked browser extension directly fromextensions/chrome/orextensions/firefox/.
Those two modes share the same local runtime behavior, but the source repository also contains release tooling, docs, site assets, and extension source files that reference public URLs for packaging and documentation workflows.
A complete local OpenClaw deployment needs all of the following:
plugins.load.pathspoints to the bundle or repo-rootopenclaw-plugindirectory.plugins.entries["js-eyes"].enabledistrue.tools.alsoAllow: ["js-eyes"]or an equivalenttools.allowentry is present, becausejs-eyesregisters optional plugin tools.- The browser extension is configured to connect to the chosen
serverHost/serverPort.
Without the tool allowlist step, the plugin can load successfully while its
single js-eyes router tool remains unavailable to the model.
Base runtime behavior:
- OpenClaw plugin: connects via WebSocket to
ws://serverHost:serverPortand uses HTTP only for JS Eyes server endpoints such as/api/browser/statusand/api/browser/tabs. - Client SDK and browser extension: connect only to the JS Eyes server URL you configure.
- Server: listens on a single HTTP+WebSocket port and does not need outbound internet access for the core browser automation loop.
By default this is all local traffic. No browser content is sent to a third-party service unless you explicitly point JS Eyes at a remote server you control.
JS Eyes 2.4+ ships an optional Native Messaging host (com.js_eyes.native_host) that lets the browser extension read ~/.js-eyes/runtime/server.token directly, avoiding manual copy-paste.
Threat model: this feature is designed to defend against external web-page / cross-origin attackers only. A compromised local device (root, malicious local process, malicious locally-loaded extension) is explicitly out of scope — any attacker with local code execution already has direct read access to server.token.
Simplifications driven by this scoped threat model:
- No in-extension secondary confirmation prompt.
- No handshake / nonce / device-fingerprint binding.
- The host simply returns the token when the browser calls it.
Trust boundaries that remain in place:
- Native messaging manifests whitelist specific extension IDs (Chrome:
allowed_origins, Firefox:allowed_extensions) — unlisted extensions cannot launch the host. - The host only reads a fixed path (
~/.js-eyes/runtime/server.token) and accepts only two messages (ping,get-config). - Extensions never expose the token through
externally_connectable, so ordinary web pages cannot read it. - The Chrome manifest pins a stable extension ID via a
keyfield so the allowlist stays authoritative after rebuilds.
See docs/native-messaging.md for install/uninstall commands and file-system paths.
Some features intentionally access external URLs, but only when the user or agent explicitly chooses those workflows:
- Extension skill discovery/install:
js-eyesactionsskills/discoverandskills/plan-install, plus the install scripts, may fetch the configured registry URL such ashttps://js-eyes.com/skills.json. - Release, docs, and packaging workflows in the source repo: development tooling may reference GitHub Releases, project websites, Cloudflare deployment targets, Mozilla AMO, or similar public endpoints.
- Browser automation targets: once connected, JS Eyes can automate whatever websites the user asks it to open; that traffic is the intended workload, not telemetry.
These are different from hidden analytics or call-home behavior. They happen only when the corresponding feature is invoked.
Static analysis tools, including VirusTotal Code Insight, often flag projects that:
- use
fetch()orWebSocket - build URLs dynamically, such as
http://${host}:${port}/api/... - expose an API or automation surface
- include installer scripts or release/download URLs in the repository
In JS Eyes, those patterns map to local browser automation, optional skill installation, or developer-facing release workflows. They are not used for silent telemetry or covert outbound control.
The ClawHub-distributed skill bundle is narrower than the full source repository:
- Included in the bundle: the runtime pieces needed for JS Eyes skill/plugin behavior.
- Not shipped in the ClawHub bundle: browser extension source, most docs, tests, and release/publishing tooling.
That means a scan of the full repository can surface external URLs that are irrelevant to the base ClawHub runtime package.
- Review the behavior in context: the most common triggers are the local automation patterns above, not remote-control malware behavior.
- Report a false positive: use the VirusTotal false positive process for the specific vendor(s) that flagged the file.
- Use manual review when needed: if you maintain an internal allowlist or review process for OpenClaw/ClawHub skills, JS Eyes is a good candidate for a reviewed exception because its behavior is inspectable and mostly local-first.
- Core runtime dependency:
wsis required for WebSocket communication. - Full development repository: includes additional packages, build tools, docs, and browser extension assets needed for local development and release workflows.
JS Eyes 2.2.0 treats skill packages as untrusted inputs that must be validated end-to-end before they reach disk or are loaded into the runtime.
- Registry metadata carries integrity data. Every entry in
dist/skills.jsonnow ships withsha256andsize. The CLI (js-eyes skills install), the OpenClawjs-eyesactionskills/plan-install, andinstall.sh/install.ps1all refuse to install a skill whose downloaded bundle does not match the expected digest. @mainfallback URLs are refused. The installers strip any registry fallback URL that resolves to a mutable@main/refs/heads/mainCDN path. Bundles must be served from an immutable tag, release, or commit pinned URL.- Safe ZIP extraction.
packages/skill-install/zip-extract.js(@js-eyes/skill-install/zip-extract) replacesexecSync unzip/ PowerShellExpand-Archivewith an in-process ZIP reader that rejects Zip Slip, symlinks, and oversized entries (maxFileSize,maxTotalSize,maxEntries). - Lockfile +
npm ci --ignore-scripts.installSkillDependenciesrequirespackage-lock.jsonand runsnpm ci --ignore-scripts --no-audit --no-fund. The flagsecurity.requireLockfile=false(orJS_EYES_REQUIRE_LOCKFILE=0in the install scripts) can be used to relax this during migration; doing so prints a prominent warning. - Plan → approve installation.
js-eyes skills install --planstages the extracted bundle in a temporary directory and writes a plan JSON toruntime/pending-skills/<skillId>.json. The install is only applied afterjs-eyes skills approve <skillId>.js-eyesactionskills/plan-installinside OpenClaw likewise produces a plan and requires an out-of-band approval via the CLI. - Runtime integrity pinning. Every installed skill gets a
.integrity.jsonmanifest that records SHA-256 for each file.registerLocalSkillsrefuses to load a skill with mismatched/missing files.js-eyes skills verifyandjs-eyes doctorsurface tamper indicators. - Skills default disabled on upgrade.
isSkillEnabledreturnsfalseunless explicitly opted in viaskillsEnabled.<id>=true. When upgrading from 2.1.x, existing skills without an explicit setting are left disabled and a warning is logged with instructions tojs-eyes skills enable <id>.
The local JS Eyes server no longer treats every localhost client as trusted.
- Bearer tokens. On first start (or via
js-eyes server token init) the server writes a random token toruntime/server.token(POSIXchmod 0600; on Windows,icaclsrestricts to the current user). Clients send the token via one of:- HTTP
Authorization: Bearer <token> - WebSocket subprotocol header
Sec-WebSocket-Protocol: bearer.<token>, js-eyes(browser extension) orjse-token.<token>(SDK) - URL query parameter
?token=<token>(legacy/custom loopback-only fallback; logged to the audit trail) Tokens can be rotated withjs-eyes server token rotate.
- HTTP
- Origin whitelist and CORS. HTTP and WebSocket upgrades require an
Originfromsecurity.allowedOrigins. The defaults cover the bundled browser extensions,http://localhost:18080, andhttp://127.0.0.1:18080.Access-Control-Allow-Originnow echoes the caller only when it is on the whitelist;*is no longer returned. - Loopback binding. The server refuses to bind to a non-loopback host unless
security.allowRemoteBind=true. When bound to a public address, a warning is logged and audited. allowAnonymouscompatibility switch. For clients that cannot yet send a token (for example, older DeepSeek Cowork installs), the operator can setsecurity.allowAnonymous=true. Anonymous connections are marked in the audit log and injs-eyes doctor. This is explicitly a migration crutch: the log line reads[js-eyes] WARNING: allowAnonymous=true; server accepts unauthenticated WS/HTTP clients.- Structured audit log. Connection events, skill installs, config edits, and sensitive tool calls are written as JSONL to
logs/audit.logwithchmod 0600.js-eyes audit tailstreams the last entries; sensitive values (cookies, script bodies, tokens) are redacted before being logged. - File permissions.
config.json,runtime/server.token,logs/audit.log, andruntime/pending-consents/*.jsonare created/rewritten withchmod 0600(best-efforticaclson Windows).
Built-in and skill-provided tools that can exfiltrate or mutate browser state are now routed through a consent gateway before execution.
- Sensitive action set.
protocol.SENSITIVE_TOOL_NAMEScurrently containsbrowser/execute-script,browser/get-cookies,browser/get-cookies-by-domain,browser/upload-file,browser/inject-css, andskills/plan-install. Additional actions can be added viasecurity.toolPolicies. - Policy modes. Each sensitive tool resolves to one of
allow,confirm, ordeny. The OpenClaw plugin records every decision underruntime/pending-consents/<id>.jsonand logs a structured warning.denyshort-circuits execution and returns a rejection payload.confirmcreates a pending record and blocks the call until the operator runsjs-eyes consent approve <id>; the approval is bound to the action and parameter digest and is consumed by the matching retry. - Extension-side eval lockdown.
handleExecuteScript/handleExecuteScriptRequest(Chrome MV3 + Firefox MV2) reject raw JavaScript payloads unlesssecurityConfig.allowRawEval=true. Starting with v2.5+, the extension no longer requires an independent config toggle: the host'ssecurity.allowRawEvalis pushed down at WebSocket handshake (init_ack.serverConfig.security.allowRawEval) and applied automatically. The extension storage keyallowRawEvalis retained as an explicit opt-out override for security-hardened deployments: if an operator sets it explicitly viachrome.storage.local.set({allowRawEval:false})(ortrue), that value wins over the host-synced value. Chrome executes approved arbitrary source through its isolateduserScriptsworld (Chrome 135+), rather than CSP-blocked extensioneval; Chrome 138+ also requires the browser-controlled Allow User Scripts toggle.RAW_EVAL_DISABLEDandUSER_SCRIPTS_UNAVAILABLElet callers degrade gracefully. - First-class page interaction vs raw eval. Wire actions
click,fill,scroll, andwait_forrun fixed, extension-ownedscripting.executeScriptfunctions with structured arguments. They do not accept caller-supplied JavaScript and do not requireallowRawEval. Prefer these (and MCP safe-profilebrowser_click/browser_fill/browser_scroll/browser_wait_for) for ordinary UI automation. Keepexecute_scriptfor cases that truly need arbitrary code, behind the full MCP profile and host policy. - Consent log review. Operators should periodically review
runtime/pending-consents/*.jsonand the JSONL entries inlogs/audit.log.js-eyes consent listsummarizes recent decisions;js-eyes consent approve <id>/js-eyes consent deny <id>mark pending entries for audit. - Server-supplied token propagation. The browser extension popup exposes a "Server Token" field that is persisted in
chrome.storage.local. The background service worker forwards the token asSec-WebSocket-Protocol: bearer.<token>and does not duplicate it into the WebSocket URL. The server still accepts the loopback query form for older/custom clients.
Starting with 2.3.0 JS Eyes ships a declarative, non-interactive policy engine that sits between any tool caller (OpenClaw plugin, CLI, skill code, external agent) and the browser. It is tuned to defuse prompt-injection-driven misuse without relying on synchronous confirm dialogs.
- L4a — Same-Origin Task (
TaskOriginTracker). Merges a scope set from four sources: user messages (URLs / bare domains),skill.contract.runtime.platforms, the current active tab URL, and links found on HTML that the agent has already fetched.getCookies/getCookiesByDomain/executeScript/injectCss/uploadFileToTabare evaluated against this scope. - L4b — Lightweight Taint (
TaintRegistry). Every cookie value returned bygetCookies*is tagged with an 8-byte canary (__canary: "jse-c-<hex>") and registered. Subsequent sink parameters (openUrl,uploadFileToTab,executeScript,injectCss) are scanned for the canary or common-encoded cookie-value variants. Hits are soft-blocked and audited asreason: 'taint-hit'. - L5 — Egress Allowlist (
EgressGate).openUrltargets must be in: the task origin scope,security.egressAllowlist(static config), or the session allowlist (populated by prior approvals / explicit user-message URLs). Unmatched targets write aruntime/pending-egress/<uuid>.jsonrecord and return{ status: 'pending-egress' }; the browser extension never sees the navigation. - L6 — Rule Engine Location. The engine lives in
@js-eyes/policyso that skills, the client SDK, and the OpenClaw plugin share one implementation.@js-eyes/client-sdkre-exports selected policy symbols from its main entry for convenience; new code should depend on@js-eyes/policydirectly.@js-eyes/server-core/ws-handlerinstantiates the same engine to cover raw WebSocket callers (external agents that bypassclient-sdk).
off— audit only (no blocking, no pending-egress). Useful for troubleshooting false positives.soft(default) — violating calls are not executed;openUrlreturnspending-egress, other sinks returnPOLICY_SOFT_BLOCK. Agents observe the decision and can re-plan.strict— same assoftbut with escalation paths closed (cookie-canary hits never pass, taint values never traverse sinks).
Environment and config overrides: JS_EYES_POLICY_ENFORCEMENT, config.security.enforcement, js-eyes security enforce <level>.
js-eyes security showprints the resolved policy (enforcement level, task-origin sources, egress allowlist, taint mode).js-eyes egress list|approve <id>|allow <domain>|clearmanages pending-egress plans and session/static allowlists.js-eyes doctorreports enforcement mode, pending-egress backlog, last soft-block event, top-3 blocked tool/rule pairs, and skills whoseruntime.platformsis['*'].logs/audit.logcarriesrule_decision,task_origin,taint_hit,egress_matched,enforcement,rule,reasons, andpendingIdfor every policy-related event.
packages/server-core now emits Content-Security-Policy: default-src 'none'; frame-ancestors 'none', X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, and Permissions-Policy: interest-cohort=() on every HTTP response. This closes the Chrome externally_connectable surface against any future accidental HTML response on port 18080.
- Interactive
confirmdialogs (still excluded by design). - Task profiles (L3) and reader sub-agent (L5') — remain opt-in additions on the roadmap and stay off by default.
Historically, enabling raw execute_script required flipping security.allowRawEval=true on the host and manually seeding chrome.storage.local.allowRawEval=true on the extension (no popup UI exposed the latter), so the host-side toggle was effectively a no-op in practice. Starting with 2.5.1:
- The host pushes
security.allowRawEvalto the browser extension viainit_ack.serverConfig.security.allowRawEvalat WebSocket handshake; the extension applies the value automatically. - The extension storage key
allowRawEvalis retained as an explicit opt-out override — set it totrueorfalseviachrome.storage.local.set({allowRawEval:false})(ortrue) to pin the extension regardless of the host. Useful for security-hardened deployments that want to force-disable raw eval even if the host flips it on. - Everyday users only need to touch
~/.js-eyes/config/config.json. Restart the server / OpenClaw after changing it so the extension picks up the new value on the next reconnect.
A small whitelist of security.* fields can now be swapped into the running JS Eyes server without restarting OpenClaw or the server. Server-core ships its own chokidar watcher on ~/.js-eyes/config/config.json (separate from the plugin's skill watcher) plus a server.reloadSecurity() handle that the js-eyes router action security/reload calls on demand.
- Hot-reloadable (swap takes effect on the next automation call, ~300 ms from fs write; also immediately via the
security/reloadrouter action):security.egressAllowlist,security.toolPolicies,security.sensitiveCookieDomains,security.allowedOrigins,security.enforcement. - Not hot-reloadable — server restart required (changing these appears under
ignoredin the reload summary, with a one-line warning in the gateway log):serverHost,serverPort,allowAnonymous,allowRemoteBind,allowRawEval,requireLockfile, and anything outsidesecurity.*(token rotation,requestTimeout, etc.). - Caveat — session-level egress approvals reset: when the allowlist flips, each live automation connection rebuilds its
PolicyContext, which means per-sessionjs-eyes egress approve <id>grants are dropped. Agents re-issue the approval on the nextpending-egressresponse; no action needed for standardallow <domain>edits because those are part of the static allowlist and get picked up automatically. - Operator triggers (any one is sufficient):
- Edit
~/.js-eyes/config/config.jsonand save — chokidar debounces 300 ms and firesreloadSecurity({ source: 'fs-watch' }). - Agent call: the
js-eyestool withaction: security/reload(returns{ changed, applied, ignored, generation, egressAllowlist }). - CLI preview:
js-eyes security reload— read-only dry run that prints what would be applied (CLI does not own the server event loop, so trigger #1 or #2 is required for the actual swap).
- Edit
- Observability: the audit log (
~/.js-eyes/logs/audit.log) gains three new events —config.hot-reload,config.hot-reload.error,automation.policy-rebuilt— andGET /api/browser/statusnow includesdata.policy.generation/data.policy.egressAllowlistso operators can externally confirm the live generation.
Last updated: 2026-07-24 — covers the policy engine, Native Messaging host,
host-synced allowRawEval, security hot reload, the single OpenClaw router,
and the host-neutral Skill Runtime V2 trust boundary.