Knowledge Hubบทที่ 21
Updates · อัปเดตใหม่

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.

ระดับ · LevelAPIทำอะไร · Does
เบา · Lightchrome.tabs, chrome.scriptingnavigate, inject JS, captureVisibleTab (screenshot)
แรง · Heavy — ที่ Claude ใช้chrome.debugger → CDPclick (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.

connectNativesendNativeMessage
lifetimePort อยู่ยาว · long-lived portspawn ใหม่ทุก 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.alarms reconnect.

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 ไม่ติด":

  1. Desktop host ชนะ Code — Chrome ไล่ host ตามลำดับ hardcoded; Claude.app/.../chrome-native-host ตอบ pong แม้ปิดหน้าต่าง GUI แล้ว → Claude Code ไม่ได้คิว.
  2. MCP server ไม่ต่อ socket — host สร้าง socket ถูก แต่ตัว MCP ไม่เคยต่อเข้า → tool call ตกที่ onToolCallDisconnected().
  3. 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_domains bypass ด้วย 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

  1. อยากได้แค่ web search? ใช้ WebSearch tool ของ Sonnet — อย่าแตะ chrome-native-host. ใช้ native host เฉพาะตอนต้องการ live logged-in session จริง.
  2. connectNative ไม่คุม Chrome — มันเปิด Port; extension ต่างหากที่เรียก chrome.* / chrome.debugger (CDP) ลงมือคุม.
  3. Wire = 4-byte length + UTF-8 JSON; จำ size limit host→Chrome ~1MB.
  4. Chrome spawn host เองทุกครั้ง → ถ้าต้องการ controller อายุยาว ต้องทำ socket-bridge (host เปิด unix socket).
  5. MV3: กัน service worker ตายด้วย chrome.alarms reconnect.
  6. "binary healthy แต่ extension ไม่ติด" → pkill Claude Desktop ให้สนิทแล้ว /chrome ใหม่.
  7. Security: page content = untrusted, per-domain permission, confirm sensitive action — sandbox/CORS ช่วยไม่ได้เมื่อ agent = logged-in user.