@huwatermelon/wechat-hook-channel
v0.1.2
Published
macOS WeChat Hook channel for Claude Code/mycc and OpenClaw.
Readme
wechat-hook-channel
macOS WeChat channel plugin for Claude Code and OpenClaw. It uses Frida Gadget plus the bundled onebot-wechat-hook bridge to receive WeChat messages and send replies from the selected agent runtime.
Architecture
WeChat.app <-> Frida Gadget <-> onebot-wechat-hook <-> wechat-hook-channel
|-> MCP server <-> Claude Code / mycc
|-> OpenClaw channel adapter <-> OpenClaw gatewayRequirements
| Dependency | Version | Notes |
| --- | --- | --- |
| macOS | 11.0+ arm64 | Apple Silicon target |
| WeChat | 4.1.9.52 | Offsets are version-specific |
| Claude Code | current channel-capable build | Custom channels still need the development-channel flag |
| OpenClaw | 2026.4.5+ | Optional; only needed for the OpenClaw channel adapter |
| Node.js | 20+ | Runs the published npm CLI |
| Bun | 1.x | Source checkout development and tests |
| Go | 1.21+ | Only needed when building onebot-wechat-hook from source |
| Frida | 17.x | Gadget/runtime must match the devkit used to build onebot |
| frida-core-devkit | 17.9.1 macOS arm64 | Only needed when building onebot-wechat-hook from source |
Required Local Artifacts
This repo vendors the onebot source code and the WeChat offset config, but it
does not vendor Tencent's WeChat app or Frida binaries. The npm install path
uses the optional @huwatermelon/onebot-wechat-hook-darwin-arm64 package for a
prebuilt onebot-wechat-hook binary; source checkouts can still build the same
binary locally.
You need these local artifacts:
- WeChat for macOS with
WeChatBundleVersionexactly4.1.9.52. Download the verified Tencent DMG and keep a local copy. Current public latest builds may be newer and will not match the bundled offsets. - Frida Gadget
17.9.1for macOS universal, downloaded from the official Frida release assets. - Frida core devkit
17.9.1for macOS arm64, downloaded from the same Frida release. This is the SDK/devkit that containsfrida-core.h;pip install fridaalone is not enough to compileonebot-wechat-hook. onebot-wechat-hook, either from the npm optional binary package or built locally fromwechat_chatter/onebotwith./scripts/build-onebot.sh.
The supported WeChat offset file is:
wechat_chatter/wechat_version/4_1_9_52_mac.jsonDo not continue if the installed WeChat version is different:
plutil -p /Applications/WeChat.app/Contents/Info.plist | grep WeChatBundleVersionInstall
npm Install
For normal macOS arm64 users, install the channel and prebuilt onebot bridge from npm:
npm install -g @huwatermelon/wechat-hook-channel
wechat-hook-channel doctorThe main package declares
@huwatermelon/onebot-wechat-hook-darwin-arm64 as an optional dependency. If
that binary package is unavailable in your environment, set ONEBOT_BIN to a
locally built onebot-wechat-hook path.
Source Checkout
Clone and build the bridge:
git clone [email protected]:<github-owner>/wechat-hook-channel.git
cd wechat-hook-channel
bun install
FRIDA_VER=17.9.1
FRIDA_DEVKIT="$HOME/.local/frida/devkit-${FRIDA_VER}-macos-arm64"
mkdir -p "$FRIDA_DEVKIT"
curl -L -o /tmp/frida-core-devkit.tar.xz \
"https://github.com/frida/frida/releases/download/${FRIDA_VER}/frida-core-devkit-${FRIDA_VER}-macos-arm64.tar.xz"
tar xf /tmp/frida-core-devkit.tar.xz -C "$FRIDA_DEVKIT"
./scripts/build-onebot.sh./scripts/build-onebot.sh looks for
$HOME/.local/frida/devkit-17.9.1-macos-arm64/frida-core.h by default. Set
FRIDA_DEVKIT=/path/to/frida-core-devkit only if you extracted the devkit
somewhere else. The Frida Gadget injected into WeChat and this devkit should use
the same Frida release version.
The bundled wechat_chatter directory is the source of truth for WeChat 4.1.9.52 offsets and outbound fixes. Do not replace it with an upstream checkout from /tmp/wechat_chatter.
If you want to run the Go tests for the native bridge, use the same Frida CGO
flags as the build. A plain go test ./... cannot find frida-core.h.
cd wechat_chatter/onebot
CGO_ENABLED=1 GOARCH=arm64 \
CGO_CFLAGS="-I$HOME/.local/frida/devkit-17.9.1-macos-arm64" \
CGO_LDFLAGS="-L$HOME/.local/frida/devkit-17.9.1-macos-arm64 -lfrida-core -lm -lresolv -Wl,-framework,Foundation,-framework,AppKit" \
CC=/usr/bin/gcc \
go test ./...WeChat Setup
Install WeChat 4.1.9.52 and disable auto-update in WeChat settings:
WECHAT_DMG_URL="https://dldir1v6.qq.com/weixin/Universal/Mac/xWeChatMac_universal_4.1.9.52_38814.dmg"
curl -L -o "$HOME/Downloads/xWeChatMac_universal_4.1.9.52_38814.dmg" "$WECHAT_DMG_URL"
open "$HOME/Downloads/xWeChatMac_universal_4.1.9.52_38814.dmg"Then verify the installed version:
plutil -p /Applications/WeChat.app/Contents/Info.plist | grep WeChatBundleVersionThe output must contain:
"WeChatBundleVersion" => "4.1.9.52"If it shows a newer version, replace the app with the exact 4.1.9.52 build before injecting Frida. The offsets are not compatible across WeChat patch versions.
Inject Frida Gadget:
killall WeChat 2>/dev/null || true
FRIDA_VER=17.9.1
mkdir -p "$HOME/.local/frida"
cd "$HOME/.local/frida"
curl -L -O "https://github.com/frida/frida/releases/download/${FRIDA_VER}/frida-gadget-${FRIDA_VER}-macos-universal.dylib.xz"
xz -dk "frida-gadget-${FRIDA_VER}-macos-universal.dylib.xz"
cp "frida-gadget-${FRIDA_VER}-macos-universal.dylib" /Applications/WeChat.app/Contents/Frameworks/FridaGadget.dylib
chmod +x /Applications/WeChat.app/Contents/Frameworks/FridaGadget.dylib
git clone https://github.com/Tyilo/insert_dylib.git /tmp/insert_dylib
cd /tmp/insert_dylib
gcc -framework Foundation insert_dylib/main.m -o /tmp/insert_dylib_bin
cd /Applications/WeChat.app/Contents/MacOS
/tmp/insert_dylib_bin --inplace --strip-codesig \
"@executable_path/../Frameworks/FridaGadget.dylib" WeChat
cp /path/to/wechat-hook-channel/wechat_chatter/frida-gadget/FridaGadget.config \
/Applications/WeChat.app/Contents/Frameworks/
cd /path/to/wechat-hook-channel/wechat_chatter
./frida-gadget/sign.shIf the patched app will not launch, re-sign the whole bundle:
sudo codesign --force --deep --sign - /Applications/WeChat.appStart WeChat and confirm Frida is reachable:
open -a WeChat
python3 -c "import frida; mgr = frida.get_device_manager(); dev = mgr.add_remote_device('127.0.0.1:27042'); print(dev.enumerate_processes())"Configure
Keep runtime settings in the shell, on the launch command, or in the local
ignored .env.wechat-hook file. .env.example is only a reference for
available variables; never commit real local values.
Minimum chat-listening config for each mycc / Claude Code channel process:
WECHAT_SELF_WXID=<wechat-account-logged-in-on-this-mac>
WECHAT_CHAT_ID=<one-private-chat-id-or-group-chatroom-id>For group listeners where the logged-in account also sends trigger messages itself, add one more filtering env:
WECHAT_TRIGGER_KEYWORDS=<self-message-prefix>Those two or three variables are the chat filtering contract. Other envs such
as WECHAT_HOOK_CALLBACK_PORT, WECHAT_HOOK_ONEBOT_API_URL,
WECHAT_HOOK_ONEBOT_TOKEN, WECHAT_OUTBOUND_MODE, and model/API credentials
control transport or outbound behavior; they do not select which chat is
listened to.
WECHAT_CHAT_ID replaces the common WECHAT_PRIVATE_MODE +
WECHAT_HOOK_ALLOWED_CHAT_IDS setup. The channel treats each process as a
one-chat listener:
| Scenario | WECHAT_SELF_WXID | WECHAT_CHAT_ID |
| --- | --- | --- |
| Logged-in account sends messages to itself | <desktop-self-wxid-or-alias> | <desktop-self-wxid-or-alias> |
| Logged-in helper account receives private messages from another account | <helper-account-wxid> | <sender-private-chat-id> |
| Logged-in account listens to one group | <desktop-self-wxid-or-alias> | <group-id>@chatroom |
Group filtering is scoped to the selected WECHAT_CHAT_ID:
- Messages from other group members trigger only when they @ the logged-in
account. The channel detects OneBot
atsegments that targetWECHAT_SELF_WXID; setWECHAT_SELF_NICKNAMESwhen OneBot only exposes the visible display name. - Messages sent by
WECHAT_SELF_WXIDin that group trigger only when they start with one ofWECHAT_TRIGGER_KEYWORDS. WECHAT_TRIGGER_OPEN_GROUPSis not used by this channel. Run onemyccprocess perWECHAT_CHAT_ID.
Defaults are live outbound sends, OneBot API
http://127.0.0.1:58080, token MuseBot, and voice handling enabled only when
Baidu ASR keys are present. Callback port defaults are:
| Config | Callback port |
| --- | --- |
| No WECHAT_CHAT_ID | 36060 |
| Private WECHAT_CHAT_ID | 36061 |
| Group WECHAT_CHAT_ID ending with @chatroom | 36062 |
Set WECHAT_HOOK_CALLBACK_PORT only when you need a non-default port, such as
more than one private listener or more than one group listener.
OneBot native send timeouts default to:
WECHAT_HOOK_TEXT_SEND_TIMEOUT_MS=15000
WECHAT_HOOK_IMAGE_UPLOAD_TIMEOUT_MS=300000
WECHAT_HOOK_IMAGE_SEND_TIMEOUT_MS=30000WECHAT_HOOK_IMAGE_UPLOAD_TIMEOUT_MS is intentionally longer because outbound
images wait for WeChat CDN upload completion before the final image message is
sent. Large generated PNGs can take more than two minutes on this native path.
The OneBot HTTP request also waits for the native send_image completion;
if that final WeChat task fails, OneBot retries once and then returns a non-2xx
error instead of reporting a false success. Text sends keep the shorter timeout.
WECHAT_SELF_WXID must be the ID of the WeChat account logged in on this Mac. You can read it from onebot logs or from:
curl http://127.0.0.1:58080/hook_stateIf you do not know the IDs yet, run a temporary discovery callback. Use a port
that is not used by mycc, then either point onebot at that port directly, or
add the discovery port as one fanout downstream while onebot keeps sending to
the fanout port.
# terminal A
cd /path/to/wechat-hook-channel
bun ./scripts/discover-chats.ts 36070
# terminal B
cd /path/to/wechat-hook-channel
WECHAT_HOOK_ENV_FILE=/dev/null WECHAT_HOOK_CALLBACK_PORT=36070 ./scripts/start-onebot.shIn fanout mode, discovery is simpler because onebot can keep using the stable fanout endpoint. Add one extra downstream port for discovery:
# terminal A
cd /path/to/wechat-hook-channel
bun ./scripts/discover-chats.ts 36063
# terminal B
cd /path/to/wechat-hook-channel
bun ./scripts/fanout.ts 36060 36061,36062,36063Now send one message from each WeChat conversation you want to identify. The discovery output prints:
self_id: the current Mac logged-in account. Use this asWECHAT_SELF_WXID.- private
chat_id: the contact/account ID forWECHAT_CHAT_ID. - group
chat_id: the group ID ending in@chatroomforWECHAT_CHAT_ID.
Discovery also appends JSONL to /tmp/wechat-hook-discover.log by default, so
detached screen or tmux sessions are still auditable:
tail -f /tmp/wechat-hook-discover.logEach record includes copy_env, for example
"copy_env":"WECHAT_CHAT_ID=1234567890@chatroom".
The onebot bridge also writes every message it forwards to
/tmp/wechat-hook-onebot-inbound.log by default. That log is the primary
source of truth for "did onebot receive this at all?"; discovery is only a
temporary tap when you want a separate callback endpoint. Set
WECHAT_ONEBOT_INBOUND_LOG=off to disable the onebot JSONL log, or set it to a
custom file path.
OneBot does not currently provide a reliable group-name field in callbacks, so
group selection must use the group chat_id ending in @chatroom; group names
are only useful for humans while identifying the right ID. The discovery output
and /tmp/wechat-hook-onebot-inbound.log include sender nicknames to help
identify the correct group while testing.
For self-chat mode, WECHAT_CHAT_ID is the same value as WECHAT_SELF_WXID.
Run one mycc process per chat. For multiple listeners, use one process per
WECHAT_CHAT_ID.
Register Global MCP
Use global MCP mode for mycc. If installed from npm, register the global CLI
once in ~/.claude/mcp.json:
MCP_FILE="$HOME/.claude/mcp.json"
mkdir -p "$(dirname "$MCP_FILE")"
node - "$MCP_FILE" <<'NODE'
const fs = require('fs')
const [file] = process.argv.slice(2)
const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {}
config.mcpServers ??= {}
config.mcpServers['wechat-hook'] = {
command: 'wechat-hook-channel',
args: ['mcp'],
}
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n')
NODEFor source checkouts, register this checkout once in
~/.claude/mcp.json, and start every mycc process with
--dangerously-load-development-channels server:wechat-hook.
WECHAT_HOOK_CHANNEL_ROOT=/path/to/wechat-hook-channel
BUN_PATH="$(command -v bun)"
MCP_FILE="$HOME/.claude/mcp.json"
mkdir -p "$(dirname "$MCP_FILE")"
node - "$MCP_FILE" "$BUN_PATH" "$WECHAT_HOOK_CHANNEL_ROOT" <<'NODE'
const fs = require('fs')
const [file, bunPath, channelRoot] = process.argv.slice(2)
const config = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {}
config.mcpServers ??= {}
config.mcpServers['wechat-hook'] = {
command: bunPath,
args: ['--cwd', channelRoot, './src/index.ts'],
}
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n')
NODEDo not keep an installed marketplace/plugin copy exposing the same
wechat-hook channel enabled at the same time. Claude Code can discover the
plugin copy first and bind WECHAT_HOOK_CALLBACK_PORT from the old directory.
Custom channels are still treated as development channels by Claude Code, so
--dangerously-load-development-channels server:wechat-hook is required.
Register OpenClaw Channel
OpenClaw uses the same onebot bridge and chat filtering rules, but it does not use the Claude Code MCP server. If installed from npm, link the installed package directory:
PLUGIN_ROOT="$(npm root -g)/@huwatermelon/wechat-hook-channel"
openclaw plugins install --link --dangerously-force-unsafe-install "$PLUGIN_ROOT"
openclaw plugins inspect wechat-hook-channelFor source checkouts, install this checkout as a linked local OpenClaw plugin:
openclaw plugins install --link --dangerously-force-unsafe-install /path/to/wechat-hook-channel
openclaw plugins inspect wechat-hook-channelThe force flag is only for local development installs that OpenClaw's safety scanner cannot classify automatically. Review the local checkout before using it.
Add a channels.wechat-hook block to ~/.openclaw/openclaw.json:
{
"channels": {
"wechat-hook": {
"enabled": true,
"selfWxid": "<desktop-self-wxid-or-alias>",
"chatId": "<one-private-chat-id-or-group-chatroom-id>",
"triggerKeywords": ["<self-message-prefix>"],
"outboundMode": "live",
"callbackHost": "127.0.0.1",
"callbackPort": 36062,
"onebotApiUrl": "http://127.0.0.1:58080",
"onebotToken": "MuseBot"
}
}
}For private/self-chat, use callback port 36061; for a group chat, use
36062 unless another listener already owns it. If you need more than one
OpenClaw WeChat listener, use named accounts and set a distinct callbackPort
for each account:
{
"channels": {
"wechat-hook": {
"accounts": {
"group-a": {
"enabled": true,
"selfWxid": "<desktop-self-wxid-or-alias>",
"chatId": "<group-a-id>@chatroom",
"triggerKeywords": ["<self-message-prefix>"],
"callbackPort": 36062
},
"group-b": {
"enabled": true,
"selfWxid": "<desktop-self-wxid-or-alias>",
"chatId": "<group-b-id>@chatroom",
"triggerKeywords": ["<self-message-prefix>"],
"callbackPort": 36063
}
}
}
}
}Restart and verify:
openclaw gateway restart
openclaw channels list
lsof -nP -iTCP:36062 -sTCP:LISTENOpenClaw keeps its own sessions. It does not reuse Claude Code / mycc
transcripts. Session files live under OpenClaw's state directory, normally:
~/.openclaw/agents/<agent-id>/sessions/For group chats, the same OpenClaw agent, account, and chatId resolve to the
same session across gateway restarts. For private/self-chat, OpenClaw's default
DM scope is the agent's main session. If one OpenClaw agent handles more than
one private WeChat chat, set session.dmScope to per-channel-peer or
per-account-channel-peer in ~/.openclaw/openclaw.json to avoid sharing one
private-chat context.
When one onebot process should feed both mycc and OpenClaw, keep onebot in
fanout mode and include the OpenClaw callback port:
cd /path/to/wechat-hook-channel
bun ./scripts/fanout.ts 36060 36061,36062Only one process can listen on a callback port. Stop the matching mycc
listener before assigning that port to OpenClaw. The current OpenClaw adapter
supports text inbound/outbound; Claude Code / mycc MCP mode remains the
full-feature path for image/file MCP tools.
Start onebot
Start onebot after WeChat is logged in. Onebot is the native WeChat bridge; Claude Code or mycc starts the MCP server separately through global ~/.claude/mcp.json.
For a single self/private listener, start onebot with the same
WECHAT_SELF_WXID, WECHAT_CHAT_ID, and callback port that the matching mycc
process will use. Private chat examples use port 36061; group examples use
port 36062. If you omit WECHAT_HOOK_CALLBACK_PORT,
scripts/start-onebot.sh derives that same default from WECHAT_CHAT_ID.
killall WeChat 2>/dev/null || true
open -a WeChat
for i in {1..30}; do
lsof -nP -iTCP:27042 -sTCP:LISTEN >/dev/null 2>&1 && break
sleep 1
done
cd /path/to/wechat-hook-channel
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<one-private-chat-id-or-group-chatroom-id> \
WECHAT_HOOK_CALLBACK_PORT=36061 \
WECHAT_HOOK_ONEBOT_PORT=58080 \
WECHAT_HOOK_ONEBOT_TOKEN=MuseBot \
WECHAT_HOOK_TEXT_SEND_TIMEOUT_MS=15000 \
WECHAT_HOOK_IMAGE_UPLOAD_TIMEOUT_MS=300000 \
WECHAT_HOOK_IMAGE_SEND_TIMEOUT_MS=30000 \
./scripts/start-onebot.shWith the npm CLI, use the same environment and replace the script with:
wechat-hook-channel onebot startEquivalent explicit command:
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<one-private-chat-id-or-group-chatroom-id> \
~/.local/bin/onebot-wechat-hook -type gadget -gadget_addr 127.0.0.1:27042 \
-wechat_conf /path/to/wechat-hook-channel/wechat_chatter/wechat_version/4_1_9_52_mac.json \
-send_url http://127.0.0.1:36061/onebot \
-receive_host 127.0.0.1:58080 \
-token MuseBot -conn_type http \
-send_text_timeout_ms 15000 \
-upload_image_timeout_ms 300000 \
-send_image_timeout_ms 30000For split private/group sessions, or when you want one onebot process to feed
multiple mycc listeners, keep onebot on the fanout port 36060, start
fanout, and pass WECHAT_CHAT_ID only to each mycc process:
cd /path/to/wechat-hook-channel
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_HOOK_FANOUT_PORT=36060 \
WECHAT_HOOK_ONEBOT_PORT=58080 \
WECHAT_HOOK_ONEBOT_TOKEN=MuseBot \
WECHAT_HOOK_TEXT_SEND_TIMEOUT_MS=15000 \
WECHAT_HOOK_IMAGE_UPLOAD_TIMEOUT_MS=300000 \
WECHAT_HOOK_IMAGE_SEND_TIMEOUT_MS=30000 \
./scripts/start-onebot.sh fanoutIn a second terminal, forward fanout traffic to the callback ports owned by
mycc:
cd /path/to/wechat-hook-channel
bun ./scripts/fanout.ts 36060 36061,36062With the npm CLI:
wechat-hook-channel onebot start fanout
wechat-hook-channel fanout 36060 36061,36062scripts/start-onebot.sh fanout also accepts the misspelled alias fansout.
In fanout mode the script ignores WECHAT_CHAT_ID for callback-port derivation
and uses WECHAT_HOOK_FANOUT_PORT, default 36060.
Start mycc With WeChat Hook
For mycc, keep onebot running first, then start mycc with the channel
enabled. Pass the same WeChat env values used by the matching onebot process.
If API settings are already loaded from an ignored local .env.api, omit the
first two ANTHROPIC_* assignments.
Big Account Self-Chat
Use this when the desktop WeChat account is the big account and you send messages from that same account. This is the simplest private assistant mode and does not need fanout.
Start interactively in your current terminal:
cd /path/to/claude-code
ANTHROPIC_BASE_URL=<api-base-url> \
ANTHROPIC_API_KEY=<api-key> \
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<desktop-self-wxid-or-alias> \
WECHAT_OUTBOUND_MODE=live \
WECHAT_HOOK_CALLBACK_PORT=36061 \
WECHAT_HOOK_ONEBOT_API_URL=http://127.0.0.1:58080 \
WECHAT_HOOK_ONEBOT_TOKEN=MuseBot \
./dist/mycc \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissionsSmall Account To Big Account
Use this when desktop WeChat is logged in as the small account, and the big account sends private messages to it.
Start mycc the same way:
cd /path/to/claude-code
ANTHROPIC_BASE_URL=<api-base-url> \
ANTHROPIC_API_KEY=<api-key> \
WECHAT_SELF_WXID=<helper-account-wxid> \
WECHAT_CHAT_ID=<sender-private-chat-id> \
WECHAT_OUTBOUND_MODE=live \
WECHAT_HOOK_CALLBACK_PORT=36061 \
WECHAT_HOOK_ONEBOT_API_URL=http://127.0.0.1:58080 \
WECHAT_HOOK_ONEBOT_TOKEN=MuseBot \
./dist/mycc \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissionsSmall Account Group Listener
Use this when desktop WeChat is logged in as the small account and one mycc
process should own a single group.
cd /path/to/claude-code
ANTHROPIC_BASE_URL=<api-base-url> \
ANTHROPIC_API_KEY=<api-key> \
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<group-id>@chatroom \
WECHAT_SELF_NICKNAMES=<your-display-name-in-that-group> \
WECHAT_TRIGGER_KEYWORDS=<self-message-prefix> \
WECHAT_OUTBOUND_MODE=live \
WECHAT_HOOK_CALLBACK_PORT=36062 \
WECHAT_HOOK_ONEBOT_API_URL=http://127.0.0.1:58080 \
WECHAT_HOOK_ONEBOT_TOKEN=MuseBot \
./dist/mycc \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissionsChannel Loading Options
This setup standardizes on global MCP mode. ~/.claude/mcp.json contains one
server:wechat-hook template pointing at this checkout; each mycc process
starts its own MCP server instance from that template.
Do not pass --bare, --strict-mcp-config, or --mcp-config in this mode.
Those options bypass or replace the global MCP config.
mycc Session Resume On Restart
By default, restarting mycc creates a new Claude Code session. For a single
self-chat or single private listener, add -c / --continue so mycc resumes
the most recent session in the current claude-code directory:
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<one-private-chat-id> \
WECHAT_HOOK_CALLBACK_PORT=36061 \
./dist/mycc \
-c \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissionsFor multiple mycc listeners started from the same claude-code directory, do
not use --continue blindly. --continue resumes the most recent session for
that directory, so a private listener can accidentally resume a group listener's
context. Use a fixed --resume <session-id> per listener, or run each listener
from a separate working directory so --continue has an unambiguous latest
session.
For multiple mycc listeners, keep the same global MCP entry and vary only
WECHAT_CHAT_ID. A private chat defaults to callback port 36061; a group
defaults to 36062:
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<one-private-chat-id> \
WECHAT_HOOK_CALLBACK_PORT=36061 \
./dist/mycc \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissions
WECHAT_SELF_WXID=<desktop-self-wxid-or-alias> \
WECHAT_CHAT_ID=<group-id>@chatroom \
WECHAT_HOOK_CALLBACK_PORT=36062 \
./dist/mycc \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissionsBackground Service
If you start mycc yourself in an interactive terminal, no screen or pseudo-TTY setup is needed. Use screen or tmux only when you want mycc to keep running in the background.
That TTY requirement is only for background mycc, not for WeChat, onebot, or normal interactive startup. Do not redirect mycc stdout/stderr directly to a file in this command; without a TTY, mycc switches to print mode and exits unless a prompt is provided. Reattach with screen -r mycc-wechat to view output.
screen -dmS mycc-wechat zsh -lc '
cd /path/to/claude-code
export ANTHROPIC_BASE_URL=...
export ANTHROPIC_API_KEY=...
export WECHAT_SELF_WXID=<desktop-self-wxid-or-alias>
export WECHAT_CHAT_ID=<one-private-chat-id-or-group-chatroom-id>
export WECHAT_OUTBOUND_MODE=live
export WECHAT_HOOK_CALLBACK_PORT=36061
export WECHAT_HOOK_ONEBOT_API_URL=http://127.0.0.1:58080
export WECHAT_HOOK_ONEBOT_TOKEN=MuseBot
./dist/mycc \
--dangerously-load-development-channels server:wechat-hook \
--dangerously-skip-permissions
'Single self/private listening does not need fanout as long as onebot and mycc
use the same WECHAT_CHAT_ID-derived callback port. Use fanout when one onebot
process must feed multiple Claude Code or mycc sessions, or when you want the
onebot side fixed on 36060 while each mycc process listens on its own
derived port. In that case onebot sends to fanout on 36060, and each mycc
listens on its derived port.
Official Claude Code Notes
This plugin works with official Claude Code signed in through claude.ai; model selection, billing, and account state are controlled by official Claude Code.
Terminal slash commands still work in Claude Code. WeChat-side remote slash commands such as /clear and /model are not guaranteed in official Claude Code unless the official client supports the channel command notification path. Treat those as a fork-specific convenience, not a stable plugin feature.
Features
| Feature | Status | | --- | --- | | Receive text | Supported | | Send text | Supported | | Receive images | Supported | | Send images | Supported | | Receive voice | Supported for private chats with Baidu ASR config; inbound records are saved as raw SILK before ASR | | Send voice | Supported for SILK records on WeChat 4.1.9.52 | | Receive files | Supported; passes local file path and metadata to Claude Code | | Send files | Supported through shareable file URLs when S3-compatible storage is configured | | Group mention trigger | Supported | | Article cards | URL is passed through for Claude Code to fetch |
File receiving does not rely on .doc or .docx extensions. The channel recognizes OneBot file segments and WeChat appmsg metadata such as type=6, appattach/totallen, fileext, attachid, cdnattachurl, and aeskey.
Troubleshooting
If onebot fails with script-load errors, verify the WeChat version and restart WeChat plus onebot. If messages stop after a WeChat crash, the Frida connection is stale and onebot must be restarted after WeChat is back in the main UI. Repeated unable to intercept function ... please file a bug errors usually mean onebot was restarted against an already-hooked WeChat process; stop onebot, restart WeChat, wait for 127.0.0.1:27042, then start onebot again.
If WeChat is still running but the UI stops responding and CPU is high, sample the process before restarting. A stack stuck near req2bufEnterAddr with FridaGadget.dylib usually means a native send timed out before the Frida send state was reset. Current onebot waits for native text/image completion, reports fatal native StartTask errors back to Go, and marks outbound native send unhealthy after fatal access violation / abort was called errors or native send timeouts. When /hook_state returns "native_send_unhealthy":true or /send_* returns 503, restart WeChat and onebot before sending again; onebot intentionally refuses further outbound sends in that state.
Useful checks:
curl http://127.0.0.1:58080/hook_state
lsof -nP -iTCP:27042 -sTCP:LISTEN
lsof -nP -iTCP:36060 -sTCP:LISTEN # fanout, when used
lsof -nP -iTCP:36061 -sTCP:LISTEN # private/self mycc callback
lsof -nP -iTCP:58080 -sTCP:LISTEN
tail -f /tmp/wechat-hook-onebot-inbound.logFor main-account self-chat, hook_state should include the configured account
ID, for example "my_wechat_id":"<desktop-self-wxid>", and "startTaskReady":true.
Use a phone message in that same account's self-chat as the final inbound test;
calling OneBot /send_private_msg from the Mac only verifies outbound send.
Repository Layout
.
├── .claude-plugin/
│ ├── marketplace.json
│ └── plugin.json
├── .mcp.json
├── src/ # MCP channel server
├── scripts/ # onebot/channel helper scripts
├── wechat_chatter/ # bundled WeChat hook source and offsets
└── package.json