- Home
- Plugins
- Security & Permissions
- wechat-clawbot
wechat-clawbot
alphainfix/wechat-clawbot
💬 A DeepSeek Harness plugin that brings your DSH agent into WeChat — chat with it from anywhere: native photo understanding, two-way file transfer, long-term memory, scheduled reminders, and permission approvals answered right in the chat.
Install
dsh plugin --profile web add github:alphainfix/wechat-clawbotREADME
wechat-clawbot
Connect a DeepSeek Harness (DSH) profile to your WeChat through the official WeChat ClawBot bridge (Tencent iLink API). Once bound, you can message your DSH agent from WeChat while you are away from the computer — ask it to look at files, edit them, run commands — and it replies (including permission/approval questions) in the WeChat conversation.
Status: v0.9.0 — text messages both ways; native vision (an inbound photo rides in the model's own message, and
look_at_imagehands a local one to it directly — no separate vision model, no second round trip); agent → WeChat file/image delivery; quoted (引用) message resolution; long-term memory; scheduled reminders; a settings card on 设置 → 插件 whose fields apply live; and a per-plugin model pin so the web UI's model picker no longer moves the bot. Group chats are not supported.
How it works
Phone WeChat ──► Tencent iLink cloud ──► this plugin (long-poll monitor)
◄──────────────────────────────── (text replies, approval questions)
│
▼
one fixed DSH agent session ("wechat-main")
(persisted, auto-compacted like any DSH session)
- The plugin reuses the protocol client from the official, MIT-licensed
@tencent-weixin/openclaw-weixinpackage (seeNOTICE); only the OpenClaw integration layer was replaced with a DeepSeek Harness integration. - The WeChat conversation maps to one fixed session id (default
wechat-main), so context is shared, history survives restarts, and automatic context compaction keeps token usage low. - The WeChat session's approval policy is forced to
ask: permission requests (file access, command execution, sandbox escalation) are forwarded to WeChat and you answer with同意/拒绝(oryes/no). - File/image delivery: the WeChat agent has a
send_wechat_filetool — just ask it (e.g. "把 README.md 发给我" or "生成一张图表发给我"). Files travel through Tencent's CDN with the official AES-128-ECB encrypted upload pipeline; images, PDFs, Office documents, archives etc. are supported. - The plugin runs inside the profile process: as long as the profile is
running, the bridge is up. Login is manual (scan a QR code), once per
binding; the token is stored under
$DSH_HOME/clawbotand reused on restart.
Requirements
- Node.js >= 22
- A DSH profile (e.g. the
webprofile) with a configured model provider - The WeChat mobile app with the official 微信ClawBot plugin available (设置 → 插件; WeChat may prompt you to update the app the first time you scan)
Install
# 1. Install the plugin into a profile (pnpm is required)
dsh plugin --profile web add wechat-clawbot
# 2. Restart the profile
# (stop `dsh web`, then start it again)
dsh web
The plugin starts automatically at boot (autoStart: true). To change its
configuration, add a clawbot row to the profile's cordis.patch.yml:
- id: clawbot
config:
allowFrom: [] # allowlist of WeChat user ids; [] = only the binder
sessionId: wechat-main # the fixed DSH session for all WeChat messages
forwardQuestions: false
approvalTimeoutMs: 0 # 0 = wait indefinitely for approval answers
logLevel: info
Connect (one-time, manual)
# From the profile directory (or anywhere; the state dir is shared):
clawbot login
A QR code is printed in the terminal. Scan it with WeChat → 扫一扫 and confirm. Afterwards:
- WeChat shows a new contact 「微信ClawBot」 — open it and start chatting.
- The running profile picks up the credentials automatically within a second (it watches the state directory) — no restart needed.
Other commands:
clawbot status # show the bound account
clawbot logout # unbind and delete credentials
Environment:
| Variable | Default | Meaning |
|---|---|---|
CLAWBOT_STATE_DIR | $DSH_HOME/clawbot | credential/state directory |
CLAWBOT_LOG_LEVEL | info | debug / info / warn / error |
Security
- Allowlist: by default only the WeChat user who scanned the QR code may
message the agent. Add more users via
allowFrom(their WeChat user ids appear in the logs /clawbot status). - Approvals: the WeChat session runs with approval policy
ask, so sensitive operations still require your explicit reply in WeChat. Other sessions (e.g. the local web GUI) keep their own policy. - Unknown senders are ignored silently.
Configuration reference
| Key | Type | Default | Meaning |
|---|---|---|---|
autoStart | boolean | true | start the monitor at boot when a bound account exists |
allowFrom | string[] | [] | allowed sender ids; empty = binder only |
sessionId | string | wechat-main | fixed DSH session for all WeChat traffic |
cwd | string | process.cwd() | workspace for a freshly created session |
forwardQuestions | boolean | false | forward ask_user_question / plan-review to WeChat (see note) |
approvalTimeoutMs | number | 0 | approval answer timeout; 0 = wait forever |
botAgent | string | DSH-ClawBot/0.1.0 … | bot_agent reported to iLink |
maxImageEdge | number | 2048 | long-edge pixel cap for compressed images |
imageQuality | number | 80 | JPEG quality for compressed images |
compressThresholdBytes | number | 1048576 | only compress images larger than this |
apiBaseUrl | string | https://ilinkai.weixin.qq.com | iLink API base (override for testing) |
logLevel | debug|info|warn|error | info | protocol log level |
stripEmoji | boolean | true | strip Unicode emoji from outbound text |
noticeMinBytes | number | 2097152 | announce a file before upload only above this size; 0 = always |
attachImages | boolean | true | give images to the model directly when the route accepts them |
provider | string | — | pin this session's provider; empty = follow the deployment default |
model | string | — | pin the model; empty = follow the deployment default |
reasoningEffort | string | — | pin the effort; empty = the model's own default |
mcpBridge | boolean | true | answer the /plugins/clawbot/mcp/* routes (see The Claude bridge) |
forwardQuestions: only oneuserQuestionsprovider can exist per DSH context. The Web UI registers one while a browser is attached, so this option only takes effect in deployments without an active Web provider. Approval forwarding (approval/request) is independent and always on for the WeChat session.
Settings
Most of the table above is editable at 设置 → 插件 → 微信 Bot, and a change lands on the next message with no restart and no dropped WeChat connection: the plugin diffs the committed settings and copies the hot fields into its live config, which every collaborator holds by reference and reads at the moment it is used.
The card writes into the settings document's clawbot section, which is an
override layer over the plugin's cordis row — cordis.patch.yml stays the
default, and the file only ever stores fields you actually changed. Each row
shows a 已覆盖 badge with a 重置 that drops back to the composed value.
Cold fields (sessionId, autoStart, forwardQuestions, botAgent,
apiBaseUrl, cwd) are deliberately absent from the card. They are stored
but not applied in place — apiBaseUrl would redirect a connected client
mid-flight and sessionId would point the bridge at a session it is not bound
to — so they live in cordis.patch.yml, where changing them already means a
reload.
Pinning the model
Leaving provider/model empty means the session follows the deployment
default, which is also why picking a model in the web UI used to move the bot:
that picker writes through to the global agent-default-model. Setting them
here breaks the coupling. A model id that will not resolve is refused rather
than applied, so a typo cannot brick the next message.
The Claude bridge
Two directions, and they are separate mechanisms with separate failure modes.
Outward: /plugins/clawbot/mcp/* — Claude Code drives DSH
Four routes, consumed by dsh-mcp-bridge (an MCP server, not a DSH plugin), which is what gives a Claude Code session tools to list/read/drive DSH sessions and to reach the owner on WeChat.
| Route | Method | Does |
|---|---|---|
mcp/sessions | GET | list sessions: id, title, cwd, live, running |
mcp/read | POST | tail one session's user/assistant turns |
mcp/send | POST | steer or follow up a live agent |
mcp/notify | POST | one WeChat message to the owner |
mcp/status | GET | is the bridge on, has anything called it |
The privileged half lives here rather than in the MCP server for a concrete
reason: ctx.agents holds the live agent objects, and steer()/followup() are
in-memory methods an out-of-process caller cannot reach. The WeChat send needs
the account token plus per-peer context tokens, so duplicating it outside would
mean a second copy of the credential handling.
Three properties are load-bearing and covered by tests:
notifytakes no recipient. It sends toaccount.userId— the id linked by QR login — so messaging anyone but the owner is unrepresentable, not merely discouraged.senddoes not reach WeChat. Injected messages carry source pluginclawbot-mcp, and the bridge's session listener treats any non-scheduleplugin turn as GUI-only (leavescurrentSenderunset). So a message pushed intowechat-mainis answered in the web UI only.notifyis the sole route to the phone. Changing that source string breaks the isolation.- Auth. Every route needs
Authorization: Bearer <token>from<state>/mcp-token(0600, minted on first boot).mcp/statusis deliberately unauthenticated — a browser page cannot hold a secret — so it reports the token's path, never its value.
mcpBridge: false makes all four return 403 while leaving them registered: the
flag is read per request, so it is a live kill switch in both directions.
Inward: three tools — DSH sees and steers Claude Code
Registered on the WeChat agent (restrictTools is a deny list, so they need
no allowlisting): list_claude_sessions, read_claude_session,
send_to_claude_session.
Listing and reading go straight to files, which is the stable half:
~/.claude/sessions/<pid>.json is world-readable, and
~/.claude/projects/<slug>/<sessionId>.jsonl is the transcript (found by
globbing for the id — deriving the directory name flattens both separators and
dots, which is easy to get subtly wrong).
Sending is the interesting half. There is no documented CLI for reaching a
live session: claude has no send/message subcommand, and --resume -p starts a
new run from history instead of the session the user is watching. The real
mechanism is peerProtocol 1 over /tmp/cc-socks/<pid>.sock with a 0600 key,
and claude.exe is a native binary. So rather than reverse-engineer it, this
shells out to a short-lived
claude -p "<deliver X to session Y>" --allowed-tools ListAgents,SendMessage --model haiku
and lets Claude Code do the socket work with its own implementation — protocol changes stay Anthropic's to keep working. Verified end to end.
Four things learned the hard way, all of them now pinned by tests:
- Session names drift. One session was observed renaming itself
harvard-96→harvard-35→harvard-efinside a day. So the name is resolved fresh on every send, never cached, and an ambiguous handle is an error rather than a guess. - Success needs an anchored match. A bare
/sent/iscores Claude Code's own "Session X is not reachable" and the plausible "was not sent" as a delivery. - State the run state and the queue explicitly, including when zero. The first version omitted both, and the bot answered 「都没有正在运行的任务」about a session that was mid-tool-call. An absent field does not read as unknown to a model — it reads as nothing, and the gap gets filled by invention.
- Queue accounting has three operations, not two.
enqueueadds,dequeuedelivers the head, andremovewithdraws a specific item by content. A real session measured 15/9/6 — counting only enqueue/dequeue calls that 6 pending when it is 0.
Run state is an inference: a turn is in flight when the newest main-thread
entry is an assistant message holding a tool_use, or a user entry that is
nothing but tool_result. Gated on a 180s freshness window, because a session
killed mid-tool-call keeps an unfinished last entry forever.
Known limit: a message currently waiting in the queue does not appear in the transcript. Verified, not assumed — a session had one queued while enqueue/dequeue/remove balanced to zero, with the file 7s fresh and writes shown to be prompt. A live answer would need the peer socket.
Timestamps in the transcript are UTC. Render them through
toLocaleTimeString, never by slicing the ISO string —"…T20:33:05Z" .slice(11,16)reported a 16:33 message as 20:33.
Limitations
- Group chats are not supported (official channel behaviour).
- Voice messages require a silk transcoder and are not supported.
- One bound WeChat account per state directory. Multiple accounts are tracked in the store but only the first is monitored.
- Scheduled reminders are session-local: one that comes due while the profile is not running is delivered, late, at the next start.
- The bridge runs only while the DSH profile process is running. For 24/7
operation keep
dsh webup (a login item or launchd unit). - WeChat's own bracket emoji codes (
[捂脸]) are stripped unconditionally from outbound text: they are WeChat-internal notation and arrive through this bridge as literal brackets.
Development
npm install
npm run typecheck # tsc --noEmit
npm run build # tsc → lib/, then copies src/client.js → lib/client.js
node test/regression.mjs # 55 offline checks, no model calls, no WeChat
node test/claude-peer.mjs # 29 checks against the real ~/.claude
node scripts/bot-sim.mjs # reply-style harness against the real prompt
Both suites are offline and cost nothing: no model calls, no network, no WeChat. They exit non-zero, so either can gate a build.
test/regression.mjs covers the things that used to fail silently: a strip rule
that ate a markdown link, a settings field declared "hot" but only read at boot,
an output schema that rejects a field the attachment store just started
returning, and the bridge invariants above (that notify reads no recipient,
that injected messages keep the clawbot-mcp source tag, that mcp/status
never echoes the token).
A hot field must appear as config.X in one of the source files that suite
greps — a field only read inside apply() is not hot, whatever HOT_FIELDS
claims, and the check enforces that. When adding a source file that reads
settings, add it to that list too.
test/claude-peer.mjs runs the peer tools against the real ~/.claude, because
the failure mode there is not "it crashed" but "it silently returned nothing" —
a renamed registry key, a changed transcript shape, a stale pid counted as live.
Queue accounting, the idle path and the clock are asserted against a synthetic
transcript with known answers, so they do not depend on what the machine happens
to be doing. send_to_claude_session is registered but never invoked by the
suite: it spawns a process and delivers into a real live session.
scripts/bot-sim.mjs drives the real system prompt and tool list against
scripted turns and prints what WeChat would have received — so a prompt change
can be judged without messaging yourself and without a session reset. It flags
empty-response endings, stacked vision calls, and bracket emoji codes. Add
--allow-emoji to exercise the stripEmoji: false branch.
The vendored iLink protocol layer lives in src/ilink/ (MIT, Tencent); the
DSH integration is src/bridge.ts, src/inbound.ts, src/approvals.ts,
src/monitor.ts, src/questions.ts, src/index.ts.
License
MIT. The vendored iLink client is MIT © Tencent — see NOTICE.
Related plugins
api-relay-audit
toby-bridges/api-relay-audit
dsh-auto-review
perrylink/dsh-auto-review
dsh-claude-ux
eri64/dsh-claude-ux
dsh-remote
xgone/dsh-remote