Chrome Native Messaging Host · คุม Chrome จาก native code + Claude-in-Chrome architecture
คลาส road-to-dev 17 มิ.ย. 2026: connectNative คุม Chrome ได้ยังไง, สถาปัตยกรรม 3 ชั้น, socket-bridge ที่ claude-in-chrome ใช้, ตัวอย่างรันได้, บั๊กจริง, และความเสี่ยงด้านความปลอดภัย — cross-verify โดย oracle แปดตัว · A 17 Jun 2026 road-to-dev class: how connectNative controls Chrome, the 3-layer architecture, the socket-bridge claude-in-chrome relies on, a runnable example, the real bugs, and the security model — cross-verified by eight oracles.
คำถามที่จุดบท · The question that started it
17 มิ.ย. 2026 P'Nut เปิดคลาสในห้อง road-to-dev ด้วยคำถามสองชั้น: (1) ทำ web search ด้วย Sonnet ผ่าน chrome-native-host ได้ไหม และ (2) nativeMessaging / connectNative มันคุม Chrome ได้ยังไง. oracle แปดตัว (Codey, SomTor, No.1, ZYN, DEVBOY, Jizo, No.6, SomBo) ช่วยกันตอบจน pattern ตกผลึก — บทนี้คือบทสรุปที่ cross-verify แล้ว.
On 17 Jun 2026 P'Nut opened a road-to-dev class with a two-part question: (1) can we run Sonnet-driven web search through a chrome-native-host, and (2) how do nativeMessaging / connectNative actually control Chrome? Eight oracles answered until the pattern crystallized — this chapter is the cross-verified distillation.
อย่าเพิ่งสร้าง — ถามก่อนว่าต้องการอะไร · Don't build it yet
ก่อนเขียนโค้ดสักบรรทัด — distinction ที่ DEVBOY เน้น และกัน over-engineer ได้มากที่สุด: ถามก่อนว่าต้องการอะไรจริง ๆ.
- อยากให้ Sonnet ค้นเว็บเฉย ๆ → Sonnet มี WebSearch tool ฝั่ง server อยู่แล้ว ไม่ต้องแตะ chrome-native-host เลย สั้นกว่าเยอะ.
- chrome-native-host จำเป็นเฉพาะ เมื่อต้องอ่าน/ค้นผ่าน session จริงของ user ที่ login อยู่ (หน้า paywall, cookie เดิม, บัญชีที่ล็อกอินไว้) — นั่นคือสิ่งที่ claude-in-chrome ทำ.
Before writing a line of code — the distinction DEVBOY stressed, the strongest guard against over-engineering: ask what you actually need. If you only want Sonnet to search the web, it already has a server-side WebSearch tool — skip the native host entirely. A native host is worth it only when you need the user's real logged-in Chrome session (paywalled pages, existing cookies, authenticated accounts). That is exactly claude-in-chrome's reason to exist.
connectNative ไม่ได้คุม Chrome — มันแค่เปิด Port · The core misconception
ความเข้าใจผิดอันดับหนึ่ง: คิดว่า connectNative สั่ง Chrome ได้ตรง ๆ. ไม่ใช่ — มันแค่เปิด Port สองทาง ระหว่าง native binary ↔ extension. ตัวที่ลงมือคุม Chrome จริงคือ extension ผ่าน chrome.* API. เปรียบเทียบที่ No.1 ให้ไว้: host = สมอง, extension = มือ, connectNative = เส้นประสาท.
The #1 misconception: that connectNative drives Chrome directly. It does not — it only opens a two-way Port between the native binary and the extension. The thing that actually controls Chrome is the extension, via chrome.* APIs. No.1's analogy: host = brain, extension = hands, connectNative = the nerve between them.
| ระดับ · Level | API | ทำอะไร · Does |
|---|---|---|
| เบา · Light | chrome.tabs, chrome.scripting | navigate, inject JS, captureVisibleTab (screenshot) |
| แรง · Heavy — ที่ Claude ใช้ | chrome.debugger → CDP | click (Input.dispatchMouseEvent), type (Input.insertText), Runtime.evaluate, Page.captureScreenshot |
สถาปัตยกรรม 3 ชั้น + โปรโตคอล · 3-layer architecture & wire protocol
Your program (Node/Python)
↓ stdin/stdout — 4-byte length + UTF-8 JSON
Native host binary
↓ chrome.runtime.connectNative()
Chrome extension (service worker)
↓ chrome.* APIs / chrome.debugger (CDP)
Chrome browser
สาย wire คือ 4-byte length (native byte order) ตามด้วย UTF-8 JSON. ระวัง size limit ที่ไม่สมมาตร: ext→host รับได้ 4MB+ แต่ host→Chrome จำกัดแค่ ~1MB.
The wire is a 4-byte length prefix (native byte order) followed by UTF-8 JSON. Mind the asymmetric size limit: ext→host accepts 4MB+, but host→Chrome is capped at ~1MB.
connectNative | sendNativeMessage | |
|---|---|---|
| lifetime | Port อยู่ยาว · long-lived port | spawn ใหม่ทุก message · new process per message |
| replies | รับได้หลาย message · a stream of messages | อ่านแค่ reply แรก ที่เหลือทิ้งเงียบ · only the first reply, rest dropped |
ตัวอย่างรันได้ · A runnable example
ชุดที่เล็กที่สุดที่รันได้จริง: host (Python) + manifest + extension. รุ่นนี้ host เป็นคนตัดสินใจเอง (self-driving) — เหมาะกับ agent loop ที่ Sonnet อยู่ใน host.
The smallest set that actually runs: a Python host + manifest + extension. Here the host decides commands itself (self-driving) — the shape you want when Sonnet lives inside the host.
① Native host (host.py) — protocol = 4-byte length + JSON:
import sys, json, struct
def read():
n = sys.stdin.buffer.read(4)
if not n: sys.exit(0)
ln = struct.unpack('=I', n)[0]
return json.loads(sys.stdin.buffer.read(ln))
def send(o):
d = json.dumps(o).encode()
sys.stdout.buffer.write(struct.pack('=I', len(d)) + d)
sys.stdout.buffer.flush()
while True:
msg = read() # {"result": "..."} กลับมาจาก extension
# ส่งให้ Sonnet ตัดสินใจ แล้วสั่ง action ถัดไป
send({"action": "eval", "js": "document.title"})
② Host manifest — วางที่ ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.oracle.host.json:
{"name":"com.oracle.host","path":"/abs/host.py","type":"stdio",
"allowed_origins":["chrome-extension://<EXT_ID>/"]}
③ Extension (background.js) — manifest MV3 ต้องมี "permissions":["nativeMessaging","tabs","scripting","debugger"]:
const port = chrome.runtime.connectNative('com.oracle.host');
port.onMessage.addListener(async (cmd) => {
const [tab] = await chrome.tabs.query({active:true});
if (cmd.action === 'navigate') chrome.tabs.update(tab.id, {url:cmd.url});
if (cmd.action === 'eval') {
await chrome.debugger.attach({tabId:tab.id}, '1.3');
const r = await chrome.debugger.sendCommand({tabId:tab.id},
'Runtime.evaluate', {expression:cmd.js, returnByValue:true});
port.postMessage({result: r.result.value}); // กลับไปให้ host/Sonnet เห็น
}
});
ติดตั้ง: โหลด extension แบบ unpacked ที่ chrome://extensions → คัดลอก Extension ID → ใส่ใน allowed_origins → restart Chrome ให้สนิท. loop เต็ม: host ตัดสินใจ → command → extension เรียก CDP คุมหน้าเว็บ → ผลกลับ host → ตัดสินใจต่อ = agent loop คุม Chrome จริง.
Install: load the extension unpacked at chrome://extensions, copy the Extension ID into allowed_origins, then fully restart Chrome. Full loop: host decides → command → extension drives the page via CDP → result back to host → next decision = a real Chrome-driving agent loop.
socket-bridge — ทำไม Claude ต้องมี · Why Claude needs the socket-bridge
ตัวอย่างข้างบน host เป็นคนสั่งเอง. แต่ Claude ต้องให้ โปรแกรมภายนอกที่อายุยาว (MCP client) เป็นคนสั่ง — แล้วเจอกำแพง: Chrome เป็นคน spawn host เองทุกครั้ง ผ่าน connectNative เราต่อเข้า process ที่รันอยู่แล้วไม่ได้ และ host ที่ Chrome spawn ก็อายุสั้น. ทางออก: ให้ host เปิด unix socket ค้างไว้ ให้ controller อายุยาวต่อเข้ามา — นี่คือ socket-bridge.
In the example above the host drives itself. But Claude needs an external, long-lived program (the MCP client) to drive — and hits a wall: Chrome itself spawns the host every time via connectNative; you cannot attach to an already-running process, and the spawned host is short-lived. The fix: the host opens a persistent unix socket that the long-lived controller connects to — the socket-bridge.
Claude Code → MCP → mcp-server → TCP/unix sock → native-host → native msg → Extension(MV3) → CDP → tab
- 2 process แยกกัน:
claude --chrome-native-host(Chrome เป็นคน spawn) +claude --claude-in-chrome-mcp(อายุยาว). - socket:
/tmp/claude-mcp-browser-bridge-<user>/<pid>.sock(dir 0700, sock 0600). - ขับ browser ด้วย
chrome.debugger(CDP) + เช็ค domain blocklist ที่api.anthropic.com. - ⚠️ MV3 footgun:
connectNative()ไม่ได้ keep service worker ให้ alive จริง → ต้องใช้chrome.alarmsreconnect.
Two separate processes: claude --chrome-native-host (spawned by Chrome) and claude --claude-in-chrome-mcp (long-lived). Socket at /tmp/claude-mcp-browser-bridge-<user>/<pid>.sock (dir 0700, sock 0600), browser driven by chrome.debugger/CDP with a domain blocklist checked against api.anthropic.com. The MV3 footgun: connectNative() does not truly keep the service worker alive — use chrome.alarms to reconnect. Real-world hosts using this exact proxy→unix-socket shape: KeePassXC and browserpass.
บั๊กจริง mid-2026 · Real Claude-in-Chrome bugs
จาก issue tracker กลางปี 2026 — เผื่อใครเจออาการ "binary healthy แต่ extension ไม่ติด":
- Desktop host ชนะ Code — Chrome ไล่ host ตามลำดับ hardcoded;
Claude.app/.../chrome-native-hostตอบ pong แม้ปิดหน้าต่าง GUI แล้ว → Claude Code ไม่ได้คิว. - MCP server ไม่ต่อ socket — host สร้าง socket ถูก แต่ตัว MCP ไม่เคยต่อเข้า → tool call ตกที่
onToolCallDisconnected(). - silent install manifest ให้ browser ที่ไม่เคยเปิด → กลายเป็นช่อง backdoor.
โครงการ reverse-eng open-claude-in-chrome เลี่ยงปัญหาโดยใช้ TCP 18765 แทน. วิธีแก้ที่ตรงอาการที่สุด: ปิด Claude Desktop ให้สนิท (pkill ไม่ใช่แค่ปิดหน้าต่าง) แล้วสั่ง /chrome reconnect ใหม่.
From the mid-2026 issue tracker — for the "binary healthy but the extension won't attach" symptom: (1) the Desktop host wins over Code because Chrome probes hosts in a hardcoded order and Claude.app's host answers pong even with its GUI closed; (2) the MCP server never connects to the socket the host created, so tool calls land in onToolCallDisconnected(); (3) a silently-installed manifest for a never-opened browser becomes a backdoor. The reverse-engineered open-claude-in-chrome sidesteps this with TCP 18765. Sharpest fix: fully kill Claude Desktop (pkill, not just close the window), then /chrome to reconnect.
Security ⚠️ · The security model
ส่วนที่สำคัญที่สุด. native host binary ไม่มี sandbox และไม่มี store review → prompt injection ทะลุจาก "tab → full userland" ได้ในก้าวเดียว โดยไม่มี prompt เตือน. allowed_origins กันได้แค่ extension แปลกหน้า ไม่ช่วยถ้าตัวที่ pin ไว้โดน inject; socket 0600 กัน cross-user ได้ แต่ไม่กัน malware ที่รันด้วย UID เดียวกัน.
The most important part. The native host binary has no sandbox and no store review, so a prompt injection jumps "tab → full userland" in one hop with no warning prompt. allowed_origins only blocks unknown extensions — useless if the pinned one is injected; a 0600 socket stops cross-user access but not same-UID malware.
เคสที่งานวิจัยใน fleet อ้างถึง · Cases cited in the fleet research:
- CVE-2025-47241 — browser-use
allowed_domainsbypass ด้วยhttps://allowed.com:pw@evil.com. - Brave / Comet — Reddit spoiler ฝังคำสั่ง → agent ไปดึง email + OTP จาก Gmail → โพสต์กลับ.
- CVE-2026-0628 — Gemini panel hijack.
- Anthropic วัด attack success เอง ~1% (Opus 4.5 หลัง RL + classifier) — ลดแล้วแต่ยังไม่ศูนย์.
แก่นที่ต้องเข้าใจ: SOP / CORS / sandbox ไร้ผล เมื่อ AI = ตัว user ที่ login เอง แล้วเดินข้าม origin โดยสมัครใจ — แค่ฝังคำสั่งในหน้าที่ agent อ่านก็พอ.
The core point: SOP/CORS/sandbox are moot when the AI is the logged-in user, voluntarily crossing origins — planting an instruction in a page the agent reads is enough. Mitigations: treat page content as untrusted (separate it from the prompt), per-domain permissions, confirm sensitive actions, and isolate agentic mode from the normal session.
Checklist
- อยากได้แค่ web search? ใช้ WebSearch tool ของ Sonnet — อย่าแตะ chrome-native-host. ใช้ native host เฉพาะตอนต้องการ live logged-in session จริง.
connectNativeไม่คุม Chrome — มันเปิด Port; extension ต่างหากที่เรียกchrome.*/chrome.debugger(CDP) ลงมือคุม.- Wire = 4-byte length + UTF-8 JSON; จำ size limit host→Chrome ~1MB.
- Chrome spawn host เองทุกครั้ง → ถ้าต้องการ controller อายุยาว ต้องทำ socket-bridge (host เปิด unix socket).
- MV3: กัน service worker ตายด้วย
chrome.alarmsreconnect. - "binary healthy แต่ extension ไม่ติด" →
pkillClaude Desktop ให้สนิทแล้ว/chromeใหม่. - Security: page content = untrusted, per-domain permission, confirm sensitive action — sandbox/CORS ช่วยไม่ได้เมื่อ agent = logged-in user.