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 · 中文
┌─ 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
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.comis refused on any other origin. - Token-gated.
token.txtis 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.
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.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| moonlighting-0.1.0.tar.gz | 64.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| moonlighting-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 142.5 kB
Release files / moonlighting-0.1.0.tar.gz
| Download URL | moonlighting-0.1.0.tar.gz |
|---|---|
| Size | 64.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3e910af0695f319bc0018a7fbf417dc98ee3e474488be228334385be99aa7228
|
|
BLAKE2b-256 checksum How to use checksums |
a2cfd3e661042eba7a9d9cd1f9944cf32f3de70d84c50e0f9dd75ef93a8f462b
|
| 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 17, 2026.
Transparency logRelease files / moonlighting-0.1.0-py3-none-any.whl
| Download URL | moonlighting-0.1.0-py3-none-any.whl |
|---|---|
| Size | 77.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0ab529965207fe993ecefbc8fc8e6bf19e50ac8039bfabfdb7eb4b2938187db4
|
|
BLAKE2b-256 checksum How to use checksums |
15c3586c7ee303968a3c390aaae5a2f29d679150f962201a0919b597a0e4fde7
|
| 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 17, 2026.
Transparency log