Sessions & Messages

dsh-codex-chrome

BruceZhang1993/dsh-codex-chrome · ★ 0

BruceZhang1993

Control the user's own Chrome (or Edge/Brave/Vivaldi/Opera) from DeepSeek Harness by reusing the OpenAI Codex `chrome` plugin's browser runtime: tabs, logged-in sessions, accessibility trees…

License
MIT
Added
2026-10-30
Maintenance
active
Install kind
npm
npm
dsh-codex-chrome
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-10-30T09:39:09.034Z
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add 'dsh-codex-chrome@0.1.0'

从 npm 安装预构建包

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. What the Codex "plugin" actually is
  2. Why there is an extra bridge process
  3. Install
  4. DSH Desktop
  5. CLI profiles
  6. Tools
  7. Implementation notes
  8. Self-checks
  9. Requirements
  10. Licence and provenance

CI License: MIT Node.js Version DeepSeek Harness plugin MCP Codex plugin

English · 简体中文

Control your own Chrome from DeepSeek Harness by migrating OpenAI Codex's chrome@openai-bundled plugin into DSH. The agent gets a set of native mcp__chrome__* tools and can reuse the browser state you already have — open tabs, cookies, logged-in sessions — to navigate, read pages, click, type and take screenshots.

DSH capturing a page from the user's Chrome via chrome_observe

Above: a DSH session calling chrome_observe { mode: "screenshot", fullPage: true } and getting a capture of the page open in the user's own Chrome.

What the Codex "plugin" actually is

It is enabled in ~/.codex/config.toml as:

[plugins."chrome@openai-bundled"]
enabled = true

But it is not an MCP server. It is three things:

Part Location Role
Skill ~/.codex/plugins/cache/openai-bundled/chrome/<ver>/skills/control-chrome/SKILL.md Tells the model to select a browser, observe with ax.write(), then act
Hooks plugin.json → Stop / Interrupt / SubagentStop Call the node_repl tool turn_ended each turn to release browser control
Runtime scripts/browser-client.mjs + scripts/browser-service.mjs The actual browser control, hosted by Codex's node_repl MCP server as a trusted service

browser-client.mjs drives the work through globalThis.nodeRepl.rpc("browser", …), and that service only runs inside the Codex node_repl host: it depends on privileged capabilities that exist nowhere else (nativePipe, addTurnEndedHandler, createElicitation, host-injected turn metadata). This migration therefore does not rewrite browser control — it reuses the vendor runtime exactly as shipped.

Why there is an extra bridge process

Every browser call must carry Codex turn metadata:

{ "_meta": { "x-codex-turn-metadata": { "session_id": "…", "turn_id": "…", "call_id": "…" } } }

The browser service reads it from nodeRepl.requestMeta and hard-fails without it:

Missing required Codex turn metadata: session_id, turn_id

@deepseek-ai/dsh-mcp-client has no per-call _meta support — its only call site is client.callTool({ name, arguments }, { … }) — so bin/chrome-mcp.mjs fronts node_repl, injects the metadata, and republishes the browser API as native DSH tools:

DSH ──mcp-client(stdio)──▶ bin/chrome-mcp.mjs ──MCP(stdio) + _meta──▶ Codex node_repl
                                  │                                        │
                                  │                             trusted service browser-service.mjs
                                  │                                        │
                                  └────────── native tools ◀────── ChatGPT browser extension ◀▶ Chrome

Install

DSH Desktop

Open the plugin manager and install the bundle from this directory, or from a registry once published. Desktop profiles are managed exclusively by the app, so dsh plugin --profile desktop add … is refused by design.

CLI profiles

dsh plugin --profile web add /absolute/path/to/dsh-codex-chrome

Either way the profile's package.json ends up with the dependency and the bundle entry; doing it by hand is equivalent:

"dependencies": { "dsh-codex-chrome": "link:/path/to/dsh-codex-chrome" }
"dsh": { "profile": { "bundles": [ "…", "dsh-codex-chrome" ] } }

then run pnpm install in the profile directory. After a restart the model sees the eleven mcp__chrome__* tools and the control-chrome skill.

Tools

Tool Purpose
chrome_status List connected browsers, the current binding, and open tabs
chrome_select Bind a browser: chrome / edge / brave / vivaldi / opera / iab / extension / default / auto
chrome_tabs list / new / select / close / user_tabs (your own tabs) / claim (take one over)
chrome_navigate goto / back / forward / reload
chrome_observe state (accessibility tree, preferred) / dom / visible_dom / screenshot / both
chrome_act via:"ax": click, setValue, typeText, pressKey, paste, scroll, selectText, performSecondaryAction, dragvia:"locator": Playwright locator click, fill, press, selectOption, check, waitFor, …
chrome_read innerText / textContent / allTextContents / attribute / count / isVisible / isEnabled
chrome_evaluate Evaluate a read-only expression in the page
chrome_wait Wait for url / load / timeout / selector
chrome_js Escape hatch: run JS inside the Codex runtime with agent / browser / tab in scope
chrome_end_turn Release browser control for this turn (Codex's Stop hook equivalent)

The model-facing workflow, documented in the bundled control-chrome skill, is observe → act → observe: prefer accessibility element indices over coordinates, and re-derive the indices after every action.

Implementation notes

  • State lives on globalThis, not in module scope. Every js call in node_repl is evaluated in a fresh realm that re-evaluates the same import(); module-level bindings reset, while globalThis is shared for the life of the kernel.
  • Screenshots cannot be written to disk. Once the browser service is active the kernel filesystem turns read-only (even /tmp and $CODEX_HOME return EROFS), so images go out through nodeRepl.emitImage; the bridge spools a copy on the host side.
  • Accessibility indices are bound to the call that produced them. chrome_act re-reads tab.ax.get("state") before acting so an index returned by chrome_observe is usable.
  • The _meta injection is the whole point. Without it every browser call fails.
  • The vendor capture path labels JPEG as PNG. Both the sandbox and the bridge re-derive the type from magic bytes, because DSH rejects an image whose declared type disagrees with its payload.

Self-checks

node scripts/check-package.mjs   # static package consistency, no browser needed
node --test                      # 13 tests: unit checks + a real MCP handshake
node scripts/doctor.mjs          # assets, extension, native host, node_repl
node scripts/doctor.mjs --deep   # …plus one real browser round trip
node scripts/smoke.mjs           # end to end: open a tab, observe, screenshot, evaluate, close

Requirements

  • Linux (developed and verified on Arch with Chrome 154, on both Wayland and X11).
  • The ChatGPT/Codex desktop app must have run at least once, so it materialises ~/.codex/plugins/cache/openai-bundled/{chrome,browser}/… (scripts/browser-client.mjs, browser-service.mjs, extension-host).
  • The ChatGPT browser extension installed in the target browser, with the com.openai.codexextension native messaging host registered (~/.config/google-chrome/NativeMessagingHosts/).
  • The target browser running.

Environment overrides:

Variable Meaning
CODEX_HOME Codex home directory, defaults to ~/.codex
DSH_CODEX_CHROME_NODE_REPL Absolute path to the node_repl executable
BROWSER_USE_AVAILABLE_BACKENDS Backends handed to node_repl, defaults to chrome,iab,mcpapps

Licence and provenance

This package is MIT licensed. It ships no OpenAI code: it calls browser-client.mjs / browser-service.mjs from your local Codex installation at runtime, and those proprietary files are not redistributed. See THIRD_PARTY.md.