A fast, full-screen, keyboard-first layer on top of X (Twitter) Direct Messages — inspired by Superhuman and the Inflow LinkedIn client.
- In-page Chrome extension — it never touches X's API or crypto. X does all the fetching, decryption, realtime, and sending.
- Full-screen reskin of X's DM interface — instant view switches, no entrance animations.
- Keyboard navigation —
j/kto move between conversations, reply-on-r,Tabto cycle inbox filters,pto pin/unpin,uto mark unread. - Message-request triage —
qopens requests,j/kbrowses them,Enteraccepts and drops you straight into the reply box,Escbacks out. - Command palette (
⌘K) and quick-switcher (⌘J). - Quick search across your conversations.
- Discoverable shortcuts — small keycap hints on the buttons themselves (
/,tab,Q,C,enter,esc), so you learn the keys as you click. - Toolbar button that jumps straight to your DMs.
- Experimental: agent-scriptable via WebMCP — AI agents like Claude can list, read, search, and reply to your DMs through typed tools instead of screen-scraping (details below).
- Because it rides X's own client, every conversation works — including end-to-end-encrypted XChat threads.
Everything maps to real X DM functionality — nothing is faked. X DMs have no archive / star / snooze, so xChat doesn't pretend to.
Unofficial and not affiliated with, endorsed by, or sponsored by X Corp. "X" and "Twitter" are trademarks of their respective owners.
→ Add xChat to Chrome — one click from the Chrome Web Store, then open
https://x.com/messages (or x.com/i/chat) and xChat activates automatically. Works in any
Chromium browser that can install from the store (Chrome, Arc, Brave, Edge, Vivaldi).
Note: the Web Store listing is built from the upstream project and does not include this fork's OpenSession connector. For opensession.groupnetwork.com integration, install from this repo — see below.
This fork (oceanseth/xChatHub) adds an
OpenSession connector: on opensession.groupnetwork.com
the extension relays DM tool calls from the OpenSession chat HUD to your own x.com tab, so you
can read and send X DMs (including E2E-encrypted XChat threads) without leaving the page —
messages flow browser ⇄ x.com only, never through OpenSession servers. It also attests your
logged-in X handle (xchat_whoami), which upgrades your GitHub ⇄ X identity link on
OpenSession to extension-grade verification.
The connector version isn't on the Chrome Web Store, so load it unpacked:
-
git clone https://github.com/oceanseth/xChatHub cd xChatHub npm install # postinstall runs `wxt prepare` npm run build # production build → dist/
- Open
chrome://extensions, toggle Developer mode on (top-right). - If the Web Store xChat is installed, remove or disable it first — both builds pin the same extension ID, and two copies fight over the page.
- Click Load unpacked and select the
distfolder. - Open an x.com tab (log in, keep the window visible on screen), then visit opensession.groupnetwork.com → Chat tab. The roster header shows “DMs live” when the connector is up.
To update later: git pull && npm run build, then hit the refresh icon on the xChat card in
chrome://extensions and reload your x.com and OpenSession tabs.
Everything else on this page (keyboard shortcuts, WebMCP tools, the local MCP bridge) works
identically in the fork; upstream changes are merged in regularly. This fork ships as its
own extension identity (“OpenSession xChat”, no pinned upstream key), so it can coexist
with the Web Store xChat — but only one should be enabled at a time, since both enhance the
same x.com pages. Bridge users: xchat-mcp pins upstream's extension id by default, so pass
--allow-origin chrome-extension://<your unpacked id>.
The plan is a dedicated store item so users get one-click install + auto-updates. Steps for the repo owner (can't be automated without store credentials):
- Chrome Web Store developer account (one-time $5 fee) at https://chrome.google.com/webstore/devconsole.
- Create a new item and upload the build zip (
npm run build, then zip thedist/contents). Listing name: OpenSession xChat. - For CI auto-publish on version tags, reuse upstream's
release.ymlafter setting this repo's own store credentials as Actions secrets (seescripts/cws-token.mjsfor the token flow) and updating the workflow's extension id to the new item's id. - After the item exists, optionally pin its public key in
wxt.config.ts(as upstream does) so unpacked dev builds share the store id.
npm install # postinstall runs `wxt prepare`
npm run dev # WXT dev build with HMR → dist/
npm run build # production build → dist/
npm test # unit tests (vitest)
npm run compile # typecheck (tsc --noEmit)npm run build- Open
chrome://extensions - Toggle Developer mode on (top-right)
- Click Load unpacked and select the
distfolder - Open https://x.com/messages (or
x.com/i/chat). xChat activates automatically.
To pick up code changes: npm run build again, then click the refresh icon on the xChat card
in chrome://extensions and reload the X tab. (npm run dev auto-rebuilds.)
If you also have the store build installed, disable it while working on the unpacked one —
they share the same extension id, and the store copy can shadow dist/.
npm version patch # or minor / majorThat's the whole release: it typechecks, tests, tags, and pushes, and CI takes it from there — build → GitHub Release → update submitted to the Chrome Web Store for review, which publishes to users on approval. One-time credential setup and the failure modes are in docs/RELEASING.md.
| Key | Action |
|---|---|
j / k (↓/↑) |
Move to next / previous conversation and open it |
Enter / o |
Open selected conversation · Enter accepts an open message request |
r |
Reply — focus the composer (moving never auto-focuses it) |
⌘K / Ctrl+K |
Command palette |
⌘J / Ctrl+J |
Quick switcher (fuzzy jump) |
/ |
Search |
c |
New chat |
Enter |
Send (in composer) · Shift+Enter newline |
Tab / ⇧Tab |
Cycle inbox filter (All / Unread / Direct / Groups) |
q |
Message requests (Esc goes back) |
p |
Pin / unpin the selected conversation |
u |
Mark the selected conversation as unread |
g g / G |
Top / bottom of list |
? |
Command palette (help) |
Every shortcut maps to real X DM functionality — nothing is faked. (X DMs have no archive/star/snooze, so xChat doesn't pretend to; features that would silently no-op were removed.)
xChat registers WebMCP tools on x.com
(document.modelContext, via @mcp-b/global),
so an AI agent can drive your DMs through typed tool calls instead of screen-scraping —
list conversations, read a thread, search, open, draft, send, pin, triage requests. The
tools wrap the same DOM layer as the keyboard shortcuts: X's own client still does all
fetching/crypto/sending, so E2E-encrypted threads work like any other.
WebMCP is an emerging W3C proposal (Chrome support is in origin trial), so consider this whole surface experimental — the tool layer works today, but the standard and the ways agents connect to it are still moving.
X never sees an API call. A MAIN-world content script registers the tools on the page;
each tool reads the rendered DOM or drives X's own controls (the same selectors.ts /
actions.ts used by the keyboard layer). For agents outside the browser, the optional
xchat-mcp bridge relays MCP over a localhost-only WebSocket:
Claude Code / any MCP client
│ stdio (MCP)
xchat-mcp (bridge/ — localhost-only WebSocket server, no credentials, no X access)
▲ ws://127.0.0.1:9553 (extension dials OUT; nothing listens in the browser)
xChat background worker
▲ runtime Port
content-script relay
▲ postMessage
MAIN-world WebMCP tools → X's rendered DOM & controls
Everything between your MCP client and the page is a dumb pipe: tool calls and results pass through verbatim, the bridge holds no state, and when it isn't running the extension does nothing but one quiet localhost dial with backoff. In-browser agents that speak WebMCP natively (or via bridges like MCP-B) can skip the bridge entirely and use the page tools directly.
| Tool | What it does |
|---|---|
xchat_state |
Where am I: view, open conversation, unread count |
xchat_list_conversations |
Rendered inbox/requests rows (id, title, snippet) — works from any x.com page |
xchat_search_conversations |
Fuzzy-search the rendered rows — works from any x.com page |
xchat_open_conversation |
Open a thread (SPA navigation) |
xchat_read_messages |
Read a thread's messages (sender + time inferred) |
xchat_draft_reply |
Fill the composer without sending (leaves the thread open for review) |
xchat_send_message |
Fill the composer and send — works from any x.com page |
xchat_set_inbox_filter |
All / Unread / Direct / Groups |
xchat_toggle_pin |
Pin/unpin via X's own context menu |
xchat_open_requests / xchat_close_requests / xchat_accept_request |
Message-request triage |
Sending is verified, never assumed. xchat_send_message reports success
(sent: "confirmed") only after X clears the composer and the sent bubble actually
appears in the thread; anything else is an explicit error stating the text was left as an
unsent draft. It re-asserts the text before every submit attempt (X's async draft-restore
can otherwise swap the content), and escalates through several submit mechanisms because
no single one reliably triggers X's send handler. If the tab isn't on the DM view, the
tool opens the thread, sends, and returns the tab to where it was — behind a brief visual
shield, so the page never visibly changes.
The tools work from any x.com page — list/search/read/send hop to the DM view behind
an invisible shield and put the tab back where it was. Two caveats by design: the tools
drive your real browser tab (xchat_open_conversation navigates on purpose, and
agent calls made while you're actively browsing x.com in that tab can interfere in both
directions), and the tab's window must be visible on screen — Chrome doesn't render
hidden windows, so X never mounts message content there; the tools detect this and say
so rather than timing out.
xChat ships its own tiny bridge — bridge/ (xchat-mcp), a stdio MCP server
that the extension connects out to over a localhost-only WebSocket. No extra browser
extensions, no third-party relays. The bridge only accepts connections whose Origin is
the xChat extension itself (its id is pinned), so no other local process can impersonate
the extension to read or send DMs (forks: --allow-origin chrome-extension://<your-id>):
cd bridge && npm install && npm run build
claude mcp add --scope user xchat -- node /absolute/path/to/xchat/bridge/dist/cli.jsOpen an x.com tab in Chrome (with xChat installed) and the xchat_* tools appear in
Claude Code — "read my unread DMs and draft replies" just works. If the tools are
missing, call xchat_bridge_status to see why (usually: no x.com tab open). Two gotchas:
- Keep the x.com window visible on screen (it doesn't need focus). Chrome skips rendering for hidden/minimized windows, so X never mounts message content there — reads come back with an explicit "bring the window to the foreground" error instead of data. Inbox listing/search still work hidden; reading and confirming sends don't.
- After updating/reloading the extension, reload any open x.com tabs too — extension reloads orphan the content scripts that relay to the bridge.
You can also poke the tools directly from DevTools on x.com, no bridge required:
navigator.modelContextTesting.listTools()
await navigator.modelContextTesting.executeTool('xchat_state', '{}')Every DOM hook lives in one file — src/content/selectors.ts —
and is anchored to data-testid/role, never to hashed class names. On boot, a self-check
verifies the required hooks exist and shows a small toast if X's markup has drifted, degrading
gracefully instead of breaking the page. The full-screen reskin is pure CSS injected via the
manifest, so it auto-applies across X's React re-renders with no JS.
