WeChatBridge connects a WeChat bot to agentic coding CLIs (Google's agy / Antigravity, xAI's Grok Build, OpenAI's Codex, or DeepSeek Harness' dsh). From WeChat you can send text, images, files, and voice-as-text to the active CLI, get replies back, and receive certain generated files over the WeChat CDN. Switch backends per user with /backend — no restart.
WeChat (phone) ⇄ iLink bot API ⇄ WeChatBridge ⇄ agy / grok / codex / dsh CLI
(this project) (runs tools)
The bridge process stays up and long-polls iLink. For prompts that go to a CLI, it spawns one agy / grok / codex / dsh child (single-turn) and exits that child when done — the child does not stay resident. Many slash commands (/help, /backend, /persona, …) are handled inside the bridge and never start a CLI. Only artifacts the bridge can detect under the user's allowed session paths are pushed back via CDN.
- Text, image, file, and voice (WeChat server-side transcription only) go to the active backend (
agy,grok,codex, ordsh) - Detected CLI artifacts under the per-user allowed tree can be sent back (size-capped); not every file the CLI touches
- Each WeChat user gets an isolated workspace; model / effort / mode are remembered per backend
- Runtime backend switch:
/backend agy,/backend grok,/backend codex, or/backend dsh(clears that backend's continuation state — the agy/grok continuation flag and the codexthread_id/resume state — so the next CLI turn starts a fresh session; history files on disk are not wiped immediately) - Slash commands for model, session reset, persona, and more (see below)
- Dangerous-prompt gate: a keyword list of concrete destructive patterns asks for confirmation before run
- Sender whitelist (
WECHATBRIDGE_ALLOWED_SENDERS; empty = allow all) /mcpreturns short usage text;/agentrewrites into a natural-language subagent prompt for the CLI (not a native MCP bridge)- Media over WeChat CDN with AES-128-ECB encrypt/decrypt
- Multi-instance: one codebase, set
WECHATBRIDGE_INSTANCEper process (state / session / QR paths derive from it) - Deploy templates: systemd (Linux), launchd (macOS), Task Scheduler notes (Windows)
- Linux — primary (systemd unit included)
- macOS — supported (launchd plist included)
- Windows — supported (Task Scheduler guide included)
Default data paths expand from ~ (e.g. ~/.local/share/wechatbridge/<instance>/).
- agy (default) — Google Antigravity CLI
- grok — xAI Grok Build CLI
- codex — OpenAI Codex CLI
- dsh — DeepSeek Harness CLI (one-shot
headlessprofile)
Per-user switch: /backend agy, /backend grok, /backend codex, or /backend dsh. Each backend keeps its own model / effort / mode memory and persona file layout. Global default is WECHATBRIDGE_BACKEND.
- What is dsh: DeepSeek Harness CLI (
dsh), running one-shot tasks via theheadlessprofile. - Enable / Switch: Set
WECHATBRIDGE_BACKEND=dshglobally in.env, or switch per-user in WeChat via/backend dsh. - Dependency (PyYAML): The dsh backend requires
PyYAMLto parse profile plugin configurations. If missing,/backend dshreturns a user-visible notice and preserves existing backend preferences. Install withpip install PyYAMLorpipx inject wechatbridge-cli PyYAML. - Window memory (default): The
headlessprofile creates a fresh session per invocation (session-<uuid>), so the bridge manages long-term conversation memory: recent turns (default 10 pairs, up toWECHATBRIDGE_DSH_MEMORY_CHARSchars) are saved todsh_memory.jsonland injected into every prompt./clearor/newwipes this memory to start fresh. - Persistent resume mode: Set
WECHATBRIDGE_DSH_RESUME=truefor true persistent sessions (codex-style resume). Requires the headless profile bundle to mount thedsh-bridge-runnerplugin (dsh_bridge_runner.py, which readsDSH_BRIDGE_SESSION_IDandDSH_BRIDGE_TASKfrom env). Context accumulates natively across turns without windowing./clearor/newdeletes the stored session ID to start a fresh session. Window memory injection is skipped in resume mode. - Workspace & State Isolation: Runs
dsh --profile headless -- <prompt>withcwd= per-user session directory (WECHATBRIDGE_SESSION_DIR/<user_id>). Bridge-private state files (dsh_memory.jsonland persistent session ID) are stored underWECHATBRIDGE_DSH_STATE_DIR(~/.local/share/wechatbridge/<instance>/dsh_state/), outside the child'ssession_dirtree to eliminate direct 1-level parent/sibling relative traversal (../). - Threat Model: Child processes run under the same host UID without container sandbox isolation. While 1-level relative traversal (
../) cannot reach private state, 2-level traversal (../../dsh_state/<user_id>) remains reachable on the filesystem and is accepted. Treat as trusted-user tooling. - Image and file attachments are merged into the prompt text as
@/absolute/pathmentions. Verified against dsh v0.1.1-rc.2 source:headlessprofile does not pre-read or inline @mention files at the CLI/runtime level, but passes prompt text directly to the model; the model receives system guidance on @-paths and invokes file tools (read) as needed. The bridge filters out-of-bounds mentions (@/abs,@~/x,@file://, replaced with[blocked-path]), which is a prompt text layer best-effort filter, not a sandbox boundary. - Auth / profiles are machine-wide (resolved via precedence
WECHATBRIDGE_DSH_HOME>WECHATBRIDGE_HOST_HOME/.dsh>~/.dsh, same host-fallback model as grok/codex): the child'sHOMEpoints at the per-user session dir, so the bridge always passesDSH_HOMEexplicitly. SetWECHATBRIDGE_DSH_HOMEto configure a dedicated service home with automatic session retention cleanup; when unset, it falls back to$WECHATBRIDGE_HOST_HOME/.dsh(if set) or~/.dshwithout automatic session cleanup (managed by the operator). If the bridge runs as a dedicated system user (e.g.wechatbridge), setWECHATBRIDGE_HOST_HOMEto point to the operator's home directory so CLI credentials and profiles are discovered.DSH_BIN_PATH,DSH_PROFILE, andDSH_TIMEOUTare configurable. - Status: Implemented against the published dsh CLI contract and covered by the test suite (fake CLI). The persistent resume mode requires users to configure and mount the
dsh-bridge-runnerplugin in their dsh profile bundle; plugin files are not bundled with this repository.
- Isolation: each WeChat user runs with
HOMEpointed at their own session directory. Conversation state stays there; login is machine-wide. - Auth: the session links to the host
~/.grok/auth.json(copied as a fallback), reusing the hostgrok login. The grok CLI rewritesauth.jsonvia temp-file + rename, which replaces the session symlink with a regular file; the bridge promotes such session files back to the host (atomic copy) and re-links, so refreshed tokens never get overwritten by the revoked host copy. Alternatively, setXAI_API_KEYin the bridge process environment (the grok child is given that key after env sanitizing). A grok-remote TUI session being signed in is not the same as the host CLI login. No key or token values are stored in this repository.
- Runs
codex exec --jsonfor each single turn; conversation continuation usescodex exec resume <thread_id> <prompt>with the thread id persisted per user. - Isolation: each WeChat user runs with
HOMEandCODEX_HOMEpointed at their own per-user session directory (session_dir/.codex), so sessions, logs, and caches never cross users. - Auth: the per-user session links to the host
~/.codex/auth.json(copied as a fallback), reusing the hostcodex login. The codex CLI rewritesauth.jsonvia temp-file + rename, which replaces the session symlink with a regular file; the bridge promotes such session files back to the host (atomic copy) and re-links, so refreshed tokens never get overwritten by the revoked host copy. Alternatively, setCODEX_API_KEYin the bridge process environment to authenticate. No key or token values are stored in this repository. - Status: there is currently no real Codex subscription or CLI available for live testing. The codex backend is implemented from source research, a JSONL fixture, and a fake CLI used by the test suite (which passes). Final acceptance depends on a real user running it against the actual Codex CLI.
- At least one CLI installed and signed in:
- agy on
PATH, or setAGY_BIN_PATH - and/or grok on
PATH, or setGROK_BIN_PATH - and/or codex on
PATH, or setCODEX_BIN_PATH - and/or dsh on
PATH, or setDSH_BIN_PATH(DeepSeek Harness,dsh loginrequired) - Antigravity is Google's terminal agentic coding CLI (successor to Gemini CLI). Grok Build is xAI's counterpart; Codex is OpenAI's terminal agentic coding CLI; dsh is DeepSeek Harness' CLI.
- agy on
- A WeChat account with a ClawBot / iLink bot (QR bind on first run)
- Python 3.10+
The recommended way is with pipx (Python >= 3.10 required):
pipx install wechatbridge-cliAfter installation, verify:
wechatbridge --versionDebian / Ubuntu:
sudo apt install pipxOther systems (or to get the latest version):
python3 -m pip install --user pipx && python3 -m pipx ensurepathThen start a new shell or re-source your shell config so pipx is on PATH.
If you want to hack on the source:
git clone https://github.com/dorokuma/wechatbridge.git
cd wechatbridge
pip install -e .Configuration is loaded from the first location found:
$WECHATBRIDGE_ENV_FILE— explicit path$XDG_CONFIG_HOME/wechatbridge/<instance>.env(defaults to~/.config/wechatbridge/<instance>.env)$XDG_CONFIG_HOME/wechatbridge/.env(defaults to~/.config/wechatbridge/.env).envin the repository root — deprecated (prints a warning on startup)
The instance name defaults to default; override with WECHATBRIDGE_INSTANCE.
Get the example config:
mkdir -p ~/.config/wechatbridge
curl -o ~/.config/wechatbridge/.env https://raw.githubusercontent.com/dorokuma/wechatbridge/main/deploy/wechatbridge.env.exampleThen edit ~/.config/wechatbridge/.env with your settings.
Key variables (all have defaults):
| Variable | Default | Purpose |
|---|---|---|
AGY_BIN_PATH |
agy |
path to the agy binary |
GROK_BIN_PATH |
grok |
path to the grok binary |
CODEX_BIN_PATH |
codex |
path to the codex binary |
DSH_BIN_PATH |
dsh |
path to the dsh binary |
DSH_PROFILE |
headless |
dsh profile booted for one-shot tasks |
DSH_TIMEOUT |
600 |
dsh CLI run timeout in seconds |
WECHATBRIDGE_DSH_MEMORY_TURNS |
10 |
dsh backend: recent user+assistant turns injected as context |
WECHATBRIDGE_DSH_MEMORY_CHARS |
6000 |
dsh backend: max chars of injected memory context |
WECHATBRIDGE_DSH_RESUME |
false |
dsh backend: true persistent-session mode — one dsh session per user resumed on every message (codex-style); requires the dsh-bridge-runner plugin in the headless profile; skips windowed memory when enabled |
WECHATBRIDGE_DSH_HOME |
empty | explicit DSH_HOME passed to the dsh child. Explicitly set = dedicated home + auto session cleanup; unset = fallback to WECHATBRIDGE_HOST_HOME/.dsh or ~/.dsh without auto cleanup (precedence: WECHATBRIDGE_DSH_HOME > WECHATBRIDGE_HOST_HOME/.dsh > ~/.dsh) |
WECHATBRIDGE_HOST_HOME |
empty | host home directory override for service user deployments; used as fallback base for grok (~/.grok), codex (~/.codex), and dsh (~/.dsh) |
WECHATBRIDGE_DSH_STATE_DIR |
derived | bridge-private dsh state directory (memory JSONL and persistent session IDs; defaults to ~/.local/share/wechatbridge/<instance>/dsh_state) |
WECHATBRIDGE_BACKEND |
agy |
global default backend (agy / grok / codex / dsh; overridable per user via /backend) |
WECHATBRIDGE_INSTANCE |
default |
instance name; state / session / QR paths derive from it |
WECHATBRIDGE_ALLOWED_SENDERS |
empty | comma-separated WeChat IDs (empty = allow all) |
AGY_TIMEOUT |
600 |
CLI run timeout in seconds (agy / grok / codex backends) |
WECHATBRIDGE_MAX_OUTBOUND_BYTES |
104857600 |
max file size sent back to WeChat (100 MB) |
WECHATBRIDGE_MAX_INBOUND_BYTES |
20971520 |
max inbound image/file after download (20 MB) |
WECHATBRIDGE_MAX_CONCURRENT |
4 |
global concurrent process slots; same user serial (queue does not hold a slot); extras get a busy reply (on small RAM hosts, lower this to avoid concurrent CLI memory spikes; see Memory limits) |
WECHATBRIDGE_CONFIRM_TOKEN |
y |
reply this token to approve a gated dangerous prompt |
WECHATBRIDGE_ENABLE_MCP |
true |
enable the /mcp help text command |
WECHATBRIDGE_ENABLE_SUBAGENT |
true |
enable the /agent prompt-rewrite command |
WECHATBRIDGE_ADMINS |
empty | comma-separated wxid list; admins receive WeChat notification when a new version is detected |
WECHATBRIDGE_UPDATE_CHECK |
true |
check PyPI for new versions on startup and every 24h; failures are silent |
WECHATBRIDGE_UPDATE_CHECK_INTERVAL |
86400 |
update check interval in seconds |
Full list: deploy/wechatbridge.env.example.
Why the new config location? With pipx the package is installed globally, so a
.envnext to the source no longer makes sense. The XDG base directory layout keeps your config separate and instance-aware.
wechatbridgeOn first run the bridge prints a QR code (and saves PNG under the instance data dir). Scan with WeChat to bind, then it long-polls for messages.
pipx upgrade wechatbridge-cli
sudo systemctl restart wechatbridgeOr run the upgrade script (no clone needed — fetch it with curl):
curl -fsSL https://raw.githubusercontent.com/dorokuma/wechatbridge/main/deploy/update.sh | sudo bashThe script upgrades the pipx installation and restarts the service. If the service runs as a dedicated system user (e.g. wechatbridge), running as root automatically runs pipx as that user (override with WECHATBRIDGE_USER=<user>).
Data lives under ~/.local/share/wechatbridge/<instance>/ (sessions, SQLite history, QR codes, login state) and is not touched during upgrade — your bots stay logged in and conversations are preserved.
Before upgrading a major or minor version (e.g. 1.2 → 1.3), check the corresponding section in CHANGELOG.md for breaking changes and migration steps.
First, install the bridge under the wechatbridge system user:
sudo -u wechatbridge pipx install wechatbridge-cliThen deploy the service unit:
sudo cp deploy/wechatbridge.service /etc/systemd/system/
sudo systemctl enable --now wechatbridgeMulti-instance: copy the template deploy/wechatbridge@.service and enable instances:
sudo cp deploy/wechatbridge@.service /etc/systemd/system/
sudo systemctl enable --now wechatbridge@bot2
sudo systemctl enable --now wechatbridge@bot3Each instance reads its own config file (~/.config/wechatbridge/bot2.env) and keeps state under its own data directory (~/.local/share/wechatbridge/bot2/).
The multi-instance template (deploy/wechatbridge@.service) configures cgroup memory protection:
MemoryAccounting=yes
MemoryHigh=450M
MemoryMax=512M- Why these defaults? Backend CLI child processes (such as
agy) can reach ~276 MB+ RSS during a single turn, and spike even higher during image recognition or long conversational sessions. A tight hard cap (e.g. 300 MB) on ~1 GB RAM hosts triggers aggressive page thrashing, deep swap latency, and stream interruptions (subscriber fell behind updates). The450Msoft limit (MemoryHigh) triggers gentle reclaim, while512M(MemoryMax) provides a hard ceiling preventing full-host OOM. - Overriding per-instance via drop-in: Override memory limits without modifying the base unit template:
Verify active limits:
sudo mkdir -p /etc/systemd/system/wechatbridge@bot2.service.d/ cat << 'EOF' | sudo tee /etc/systemd/system/wechatbridge@bot2.service.d/memory.conf [Service] # for hosts with >= 2 GB RAM MemoryHigh=600M MemoryMax=768M EOF sudo systemctl daemon-reload sudo systemctl restart wechatbridge@bot2
systemctl show wechatbridge@bot2 -p MemoryHigh,MemoryMax
- Sizing guidance: The defaults are tuned for small hosts (~1 GB RAM). On larger hosts with ample headroom, limits can be relaxed. When deploying multiple instances, note that each instance runs in its own service cgroup — budget for the worst-case combined ceiling of N × MemoryMax across running instances.
- Interaction with concurrency: Concurrent heavy turns (e.g. multiple users sending images at once) multiply backend CLI memory consumption. On memory-constrained hosts, prioritize lowering
WECHATBRIDGE_MAX_CONCURRENT(e.g. to2or1) before increasing memory caps. - Swap configuration: Avoid tightening
MemorySwapMax— swap margin serves as a vital safety buffer that slows execution under pressure instead of triggering an immediate cgroup OOM kill. - Single-instance deployments: The single-instance unit
deploy/wechatbridge.serviceincludes the same built-in memory limits (MemoryHigh=450MandMemoryMax=512M). To adjust them, override via a drop-in under/etc/systemd/system/wechatbridge.service.d/and replace the unit name in thesystemctlcommands above withwechatbridge. Takes effect when the unit file is installed or re-installed;update.shupgrades do not touch unit files. - Platform scope: These cgroup memory limits apply only to Linux systemd deployments; the macOS (launchd) and Windows (Task Scheduler) paths have no equivalent per-instance cap.
cp deploy/wechatbridge.plist ~/Library/LaunchAgents/com.wechatbridge.plist
# edit WorkingDirectory and ProgramArguments in the plist
launchctl load ~/Library/LaunchAgents/com.wechatbridge.plistSee deploy/wechatbridge-windows.md.
| Command | Action |
|---|---|
/help |
list supported commands for the active backend |
/backend <agy|grok|codex|dsh> |
switch CLI backend for this WeChat user (on real change: clears that backend's continuation state — agy/grok flag, codex thread_id/resume, or dsh memory/session ID — so the next turn starts a fresh session; history files may remain until retention cleanup) |
/clear or /new |
start a new conversation (agy/grok: drops continue flag; codex: clears thread ID; dsh: clears window memory or persistent session ID) |
/model <name> |
set model (all backends validate against a live list: agy/grok via CLI models; codex via codex debug models [then --bundled]; unknown name or list-fetch failure refuse and do not write prefs; see /models) |
/models |
list models — agy/grok/codex all query the live CLI (codex: debug models; falls back to a built-in reference note only if the live list cannot be fetched) |
/fast |
set low reasoning effort (on only — not a toggle; no “off” command) |
/planning |
set planning mode (on only — not a toggle) |
/add-dir <path> |
agy: pass --add-dir on later runs if path is allowed. grok: recorded only; not passed to the CLI yet |
/agents |
list agents via the active CLI |
/persona <text> |
set persona (show / clear / reset subcommands) |
/version |
show current version, instance name, and backend; if a newer version is available, show upgrade hint |
/mcp |
short MCP usage hint text (can disable with WECHATBRIDGE_ENABLE_MCP) |
/agent <name> <task> |
craft a "invoke subagent …" prompt and run the CLI (can disable with WECHATBRIDGE_ENABLE_SUBAGENT) |
Other /… commands are either rejected (e.g. /exit), reported as unsupported on WeChat (TUI-only panels), or passed through to the active CLI.
/add-dir only accepts paths under the user's session directory or roots listed in WECHATBRIDGE_ADD_DIR_ROOTS.
- Whitelist first. Empty
WECHATBRIDGE_ALLOWED_SENDERSmeans anyone who can message the bot can use it. - Auto-approve CLIs. agy runs with
--dangerously-skip-permissions; grok with--always-approve(unless planning mode); dsh tools run without host path restrictions. Treat this as trusted-user tooling, not a multi-tenant sandbox. - Danger gate is keyword-based, not full intent understanding. Defaults target concrete patterns (
rm -rf /, pipe-to-shell,mkfs,format c:, a few heavy Chinese phrases, …). Everyday wording like bare “delete” is not gated. Override list viaWECHATBRIDGE_CONFIRM_KEYWORDS; approve withWECHATBRIDGE_CONFIRM_TOKEN(defaulty), TTLWECHATBRIDGE_PENDING_TTL. - Inbound media is size-capped (default 20 MB), streamed, and CDN hosts are allowlisted. Missing
aes_keyreturns a clear error. - Outbound artifacts only leave the allowed per-user tree (agy: session scratch; grok: under session dir; dsh: under session dir), after
realpathchecks, and only if underWECHATBRIDGE_MAX_OUTBOUND_BYTES. - Concurrency: global process-slot cap (
WECHATBRIDGE_MAX_CONCURRENT, default 4). Same user is serialized and does not hold a global slot while waiting on their previous message; different users can run in parallel up to the cap (heavy concurrent tasks multiply subprocess memory; see Memory limits). - Long replies are split into chunks (
WECHATBRIDGE_MESSAGE_CHUNK, default 2000 characters). - Data layout: instance data under
~/.local/share/wechatbridge/<instance>/(override with env). Runtime dirs prefer0700; token/QR files prefer0600(Unix; Windows relies on NTFS ACLs). - Retention: session temps vs dialogue history use separate TTLs (
WECHATBRIDGE_SESSION_RETENTION_DAYS,WECHATBRIDGE_HISTORY_RETENTION_DAYS). Prefs/auth are kept. - Child env is sanitized (strips common secret-style variable names) and points
HOME(andUSERPROFILEon Windows) at the per-user session dir.
- Not a standalone agent — requires agy and/or grok and/or codex and/or dsh.
- The codex backend is not yet verified against a real Codex subscription/CLI; it is validated by source research, a JSONL fixture, and a fake CLI in tests. Treat it as community-tested until a real user confirms.
- The dsh backend supports both windowed memory (default, last
WECHATBRIDGE_DSH_MEMORY_TURNSturns) and true persistent sessions (WECHATBRIDGE_DSH_RESUME=true). Covered by the test suite; persistent resume mode requires users to configure and mount their owndsh-bridge-runnerplugin (this repository does not bundle plugin files). - dsh model/effort/mode/persona slash commands are not wired yet;
/model,/fast,/planning,/persona,/add-dirreturn a "not supported" notice on the dsh backend. - Voice is WeChat speech-to-text only; no local ASR; empty transcript → “type instead”.
- No video send/receive; no native WeChat voice-bubble replies (no silk encode).
- One WeChat binding per process; multiple accounts need multiple instances (
WECHATBRIDGE_INSTANCE). - Artifact send-back is best-effort detection under allowed paths, not “every file the CLI created anywhere”.
/mcp//agentdo not implement MCP protocol or spawn process supervisors inside the bridge — they only guide or rephrase for the CLI.- Deploy only for trusted users behind a whitelist when possible.
See CONTRIBUTING.md. Semantic Versioning from 1.0.0; record changes in CHANGELOG.md.
MIT. See LICENSE.