Skip to main content

Moonlighting

Invisible hands for your AI agent. A real Chrome, on its own hidden Windows desktop, driven by element references instead of coordinates — with credentials that never reach the model.

English · 中文

PyPI License: MIT Platform

┌─ your desktop ─────────────────────────────┐
│  Hermes / Claude / Codex / any MCP client   │
└───────────────┬────────────────────────────┘
                │ MCP (stdio)  or  local HTTP
┌───────────────▼────────────────────────────┐
│  hidden desktop "HermesDesk"                │
│  ┌──────────────────────────────────────┐   │
│  │ real Chrome + persistent profile     │   │
│  │ (logins, wallet, extensions)         │   │
│  └──────────────────────────────────────┘   │
│  credential broker (DPAPI, origin-bound)    │
└─────────────────────────────────────────────┘

You never see a window flash. The agent never sees a password.

Why

Driving a browser as an agent usually goes wrong in three places:

Problem What most setups do What this does
Finding elements screenshot + guess coordinates one accessibility snapshot, act on [ref=e42]
Losing state fresh profile every run persistent profile on a private desktop; reopen and the session is still there
Handling passwords paste into the prompt user types into a masked popup → DPAPI-encrypted locally → injected by the broker, never returned to the model

Install

Requires Windows 10/11, Python 3.10+, Node 18+ (for the Playwright MCP child), Chrome.

Option A — pip (no paths to edit)

pip install moonlighting
moonlighting-setup --options          # where should browser data live?
moonlighting-setup --home "D:\moonlighting"
moonlighting                          # starts stream + broker + driver

Then wire it into your MCP client with no absolute paths at all:

{ "mcpServers": { "browser": { "command": "moonlighting-mcp" } } }

moonlighting-mcp, moonlighting-setup, moonlighting-session and moonlighting are console scripts installed by the package.

Option B — clone

git clone https://github.com/xinyuzjj/moonlighting.git
cd moonlighting
pip install -r requirements.txt

python scripts/setup.py --options          # where should browser data live?
python scripts/setup.py --home "D:\moonlighting"
python scripts/svc.py                      # starts stream + broker + driver

Either way, install_mcp.py writes the right config for you — it detects whether moonlighting-mcp is installed and falls back to absolute script paths if not:

python scripts/install_mcp.py                 # print configs with real paths
python scripts/install_mcp.py --write claude  # write it (backs up the original)
python scripts/install_mcp.py --write codex
python scripts/install_mcp.py --write all

svc.py / moonlighting prints a preview URL. Open it in the Hermes preview pane (or any browser) to watch and even click into the hidden desktop:

preview: http://127.0.0.1:8765/ctl

First run creates <home>/token.txt — every state-changing endpoint requires it.

Use it from an agent

Any MCP client (Claude Desktop, Claude Code, Codex CLI, Hermes)

scripts/mcp_server.py is a standard MCP stdio server exposing 27 tools (25 browser_* plus cred_list / cred_fill).

If you installed with pip, the whole config is one line — no paths:

{ "mcpServers": { "browser": { "command": "moonlighting-mcp" } } }

Cloned instead? Let install_mcp.py fill in the real paths:

python scripts/install_mcp.py --write all

Or write it by hand (Claude Desktop / Claude Code, mcpServers):

{
  "mcpServers": {
    "browser": {
      "command": "python",
      "args": ["C:\\path\\to\\moonlighting\\scripts\\mcp_server.py",
               "--cdp-endpoint", "http://127.0.0.1:18763"]
    }
  }
}

Codex CLI (~/.codex/config.toml — TOML, snake_case, and give it more startup time):

[mcp_servers.browser]
command = "python"
args = ["C:\\path\\to\\moonlighting\\scripts\\mcp_server.py", "--cdp-endpoint", "http://127.0.0.1:18763"]
startup_timeout_sec = 60
tool_timeout_sec = 120

Plain HTTP

TOKEN=$(python scripts/auth.py --print | sed 's/^token: //')

curl -H "X-Hermes-Token: $TOKEN" http://127.0.0.1:8897/snap        # whole page + refs
curl -H "X-Hermes-Token: $TOKEN" "http://127.0.0.1:8897/click?target=e42&element=Sign%20in"
curl -H "X-Hermes-Token: $TOKEN" "http://127.0.0.1:8899/list"      # saved credentials (no secrets)

Multiple agents, no interference

Each session gets its own desktop, profile, and ports — two agents cannot fight over tabs or invalidate each other's element refs.

python scripts/session.py new --name a1 --url https://example.com
python scripts/session.py list
python scripts/session.py stop --name a1
a1   cdp=18764  stream=8766  driver=8898  UP  http://127.0.0.1:8766/ctl

Running more than one instance

The defaults (HermesDesk, ports 8765/8897/8899/18763, one global pointer file) are meant for a single installation. If something is already running on the machine, give the second instance its own identity — all of it is environment variables:

export HERMES_BROWSER_HOME="D:\mb-agent"          # browser data (profiles, vault, token)
export HERMES_SETTINGS="D:\mb-agent\settings.json"  # don't touch the global pointer at all
export HERMES_DESK=MBAgentDesk
export HERMES_STREAM_PORT=18770 HERMES_BROKER_PORT=18771
export HERMES_DRIVER_PORT=18772 HERMES_CDP_PORT=18773

What the code does to keep them apart:

  • The pointer file is never silently rewritten. setup.py --home refuses when settings.json already points somewhere else; --force overwrites after backing it up.
  • Ports are claimed with an identity check. Services report home + desk on /health and /status; a port whose owner reports a different identity is treated as foreign and skipped, instead of being adopted ("it answered, so it must be mine").
  • Environment survives the detach. Services are spawned through WMI so they outlive the agent, and WMI does not inherit your environment — so svc.py writes the isolation variables into a .cmd wrapper that starts the process. Without that, an isolated install silently falls back to the default home and ports.
  • No legacy fallback once you've chosen a home. With HERMES_BROWSER_HOME set, profiles/ and vault/ resolve under it, never under a previous install's paths.
  • The token exists before you need it. svc.py generates it at startup, so reading token.txt right after boot returns a value instead of an empty file.

MOONLIGHTING_NPX pins the npx binary when the MCP client starts you with a clean PATH.

Security model

  • Passwords never enter the model context. The user types into a masked popup; the broker encrypts with Windows DPAPI, binds the secret to the page origin, and injects it. Tool results only contain {ok, verified, submitted, id_len, pwd_len}.
  • Origin-bound. A credential saved for https://github.com is refused on any other origin.
  • Token-gated. token.txt is generated on first run (mode 600). Read-only probes (/health, /status, /snapshot) stay open so the preview keeps working.
  • Audited. Every injection appends one line to <home>/audit.jsonl: timestamp, action, origin, handle, result code. No plaintext, ever.
  • No CAPTCHA evasion. This drives a real browser; it does not spoof fingerprints, solve puzzles, or defeat anti-bot systems.

Layout

scripts/
  mcp_server.py      standard MCP stdio server (browser_* + cred_*)
  browser_daemon.py  persistent driver: snapshot / click / type / tabs / eval
  desk_stream.py     live view of the hidden desktop (CDP screencast) + click/type back-channel
  vault_broker.py    credential broker: DPAPI, origin check, selector fill with read-back verify
  cred_dialog.py     masked popup the user types into
  login_flow.py      full login loop: popup → fill → verify → submit → retry on failure
  desk_chrome.py     launch / close Chrome on a named private desktop
  private_desktop.py Win32 desktop primitives (CreateDesktopW, WM_SETTEXT, PrintWindow)
  session.py         per-agent isolation (desktop + profile + ports)
  svc.py             start/stop the three services silently (no console windows)
  ports.py           port allocation: if the default is taken, pick a free one
  auth.py            token file + header helper
templates/           preview pages (/ctl interactive, /view read-only)
docs/SKILL.md        the long-form playbook: every pitfall we hit, with measurements

Platform

Windows only, by design: private desktops (CreateDesktopW), DPAPI, and silent process launch are Win32 primitives. The browser-control layer itself is portable — porting means replacing private_desktop.py and the DPAPI calls.

Agent loop (experimental, agent/)

scripts/ is the hands; agent/ is a thin closed loop on top of it — not packaged in the wheel, run it from a clone.

cd moonlighting/agent
python run.py ../tasks/github-login-replay.json     # steps + assertions + retries + events.jsonl
file what it does
obs.py /snap → compressed to ≤6 KB (actionable nodes only), shot() for pixels
verify.py assertion DSL: url / text / eval / count
act.py 11 atomic actions, hard allow-list
run.py per-step retries, milestone rollback, forbid / ask_user / done_when
mem.py site memory: host+path → successful action sequence

Measured: 42 KB snapshot → 6.0 KB (-86%), empty click caught 10/10, failed step retries 3× then screenshots and rolls back. There is no planner yet: a task is an explicit step list.

Acknowledgements

Standing on these projects:

Project How it is used
microsoft/playwright-mcp The accessibility-snapshot engine. mcp_server.py proxies browser_* tools to it; its [ref=...] model is what makes coordinate-free clicking possible. (Apache-2.0)
microsoft/playwright The browser driver underneath playwright-mcp. (Apache-2.0)
python-websockets/websockets CDP transport for the driver, the stream and the broker.
python-pillow/Pillow JPEG handling for the live preview and the drawn cursor.

Design ideas borrowed, no code vendored:

  • browserbase/stagehand — observe() returning real selectors instead of pixels; that is why this project resolves elements by selector or ref and never by coordinates.
  • browser-use/browser-use — the agent loop shape: snapshot, act, verify.
  • web-infra-dev/midscene — the vision-based fallback idea for canvas-only UIs (deliberately not enabled by default; it is slower).

License

MIT. The Apache-2.0 licenses of playwright-mcp and Playwright apply to those components.

Metadata

Release files for moonlighting 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for moonlighting 0.1.2
File Size Uploaded
moonlighting-0.1.2.tar.gz 71.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for moonlighting 0.1.2
File Interpreter ABI Platform
moonlighting-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 154.6 kB

Release files / moonlighting-0.1.2.tar.gz

Download URL moonlighting-0.1.2.tar.gz
Size 71.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5073cc8f91591b8d3991a2d5d4e57d98ae35f3f3635de9704290f0b215181978
BLAKE2b-256 checksum
How to use checksums
9a1b710f13f208d073e43f0767b0fc285212ac0cc0fa8c19303563c4835cf626
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / moonlighting-0.1.2-py3-none-any.whl

Download URL moonlighting-0.1.2-py3-none-any.whl
Size 83.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
86c00c9e6725a0c168e668eabeeaa44c4e2dd3cbea14e78ec6d18dd3f7db4ad4
BLAKE2b-256 checksum
How to use checksums
b5b2daeef40f058872f33ffa936ad1d0df934937fee4a962bcb019d772f27bd3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page