Original project attribution
This repository was cloned and adapted from the original GitHub project
niyazmft/openclaw-zulip-bridge. The currentjeffsnell/openclaw-zulip-bridgerepository preserves that project history and continues development for Jeff Snell's OpenClaw/Zulip deployment.
High-performance OpenClaw channel plugin for Zulip streams and private messages with persistent event queues, traffic policies, and comprehensive observability.
- Quick Start
- Features
- Prerequisites
- Installation
- Configuration
- Verification
- Architecture
- Troubleshooting
- Known Issues
- Development
- Contributing
- Security
- License
💡 Easiest path when you already have a trusted OpenClaw agent on the target machine: ask that agent to clone this repository into a local project directory, run
pnpm installandpnpm run check, add the local clone to OpenClaw's plugin load paths, configure the Zulip bot credentials, restart the gateway, and perform a real Zulip message/reply test.
For a manual install, run:
# 1. Clone this repository
git clone https://github.com/jeffsnell/openclaw-zulip-bridge.git
cd openclaw-zulip-bridge
# 2. Install dependencies and validate
pnpm install
pnpm run check
# 3. Add the absolute local repository path to plugins.load.paths in openclaw.json
# and configure channels.zulip with your bot credentials and access policy.
# 4. Validate, restart, and verify
openclaw config validate
openclaw gateway restart
openclaw status --deepSee Installation for the OpenClaw config shape and Zulip channel settings.
- Persistent Event Polling: Automatically resumes from where it left off using locally-persisted queue metadata.
- Traffic Policies: Granular control over who can interact with the bot in DMs and Streams.
- Multiple Accounts: Support for multiple Zulip accounts and realms in a single instance.
- Mention Gating: Intelligent stream handling with
oncall,onmessage, andoncharmodes. - Durable Deduplication: Built-in persistent deduplication store to prevent duplicate message processing.
- Media Support: Automatically processes Zulip uploads and inline images.
- Rich Feedback: Optional reaction-based status indicators for request start, success, and errors.
- Standardized Observability: Machine-parseable logs for easy monitoring and troubleshooting.
- OpenClaw: Version
>=2026.6.0 - Node.js: Latest LTS recommended (Node 22+)
- Zulip Bot: A registered bot on your Zulip realm
- Log into your Zulip server
- Go to Settings → Your Bots → Add a new bot
- Choose Bot type: "Generic bot"
- Give it a name (e.g., "openclaw-bot")
- Copy the API key shown — this is your
ZULIP_API_KEY - The bot's email is your
ZULIP_EMAIL
This repository can be used as a local OpenClaw plugin by cloning it and adding the clone path to OpenClaw's plugin load paths.
git clone https://github.com/jeffsnell/openclaw-zulip-bridge.git
cd openclaw-zulip-bridgeDo not clone directly into ~/.openclaw/extensions/zulip. Use a normal project directory and load that path from OpenClaw config.
Use pnpm:
pnpm install
pnpm run checkAdd the local repository path to your OpenClaw config:
{
"plugins": {
"entries": {
"zulip": {
"enabled": true
}
},
"load": {
"paths": [
"/absolute/path/to/openclaw-zulip-bridge"
]
}
}
}Add your Zulip bot credentials to OpenClaw's channel config:
{
"channels": {
"zulip": {
"enabled": true,
"name": "Zulip Bot",
"url": "https://your-zulip-server.example.com",
"email": "bot@example.com",
"apiKey": "your-zulip-bot-api-key",
"dmPolicy": "allowlist",
"allowFrom": ["your-email@example.com", 12345],
"groupPolicy": "allowlist",
"groupAllowFrom": ["your-email@example.com", 12345],
"chatmode": "oncall",
"streams": ["*"],
"requireMention": true
}
}
}Do not commit real Zulip API keys or OpenClaw config files. Use placeholders in documentation and keep credentials in your local OpenClaw config or environment.
For allowlists, use the Zulip sender email and/or numeric Zulip user id for the human operators who should be allowed to trigger the bot. Avoid allowFrom: ["*"] and groupAllowFrom: ["*"] for normal operation when the OpenClaw agent has elevated/runtime/file tools enabled.
openclaw config validate
openclaw gateway restart
openclaw status --deepExpected result:
Zulip ON OK configured
This plugin has zero production npm dependencies. You can build it on a connected machine, then copy the folder to an offline machine:
# On the connected machine:
git clone https://github.com/jeffsnell/openclaw-zulip-bridge.git /tmp/zulip-bridge
cd /tmp/zulip-bridge
pnpm install
pnpm run checkThen on the offline machine, copy the validated folder to a local project path, add that path to OpenClaw's plugin load paths, configure channels.zulip, restart OpenClaw, and run:
openclaw status --deepRun the built-in channel onboarding wizard:
openclaw channels addThen at the interactive prompts:
- Select "Set up a chat channel now?" → Yes
- Choose "Zulip (plugin)" from the channel list
- Follow the guided prompts to enter your API key, bot email, and site URL
- Choose to route messages to an agent (e.g.,
main)
After setup, if dmPolicy is "pairing", send a DM to your bot and approve yourself:
openclaw pairing approve zulip <PAIRING_CODE_FROM_ZULIP_DM>Tip: If
ZULIP_API_KEY,ZULIP_EMAIL, andZULIP_URLare set as environment variables, the wizard uses them automatically.
For advanced users, add to your openclaw.json:
{
"plugins": {
"entries": {
"zulip": {
"enabled": true
}
},
"load": {
"paths": ["/absolute/path/to/openclaw-zulip-bridge"]
}
},
"channels": {
"zulip": {
"enabled": true,
"url": "https://your-zulip-server.example.com",
"email": "bot@example.com",
"apiKey": "your-zulip-bot-api-key",
"dmPolicy": "allowlist",
"allowFrom": ["your-email@example.com", 12345],
"groupPolicy": "allowlist",
"groupAllowFrom": ["your-email@example.com", 12345],
"chatmode": "oncall",
"streams": ["*"],
"requireMention": true
}
}
}| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable the Zulip channel |
dmPolicy |
string | "pairing" |
Who can DM the bot: "open" (anyone), "allowlist" (specific users), "pairing" (must pair first), "disabled" (ignore DMs) |
streams |
string[] | ["*"] |
Streams to monitor ("*" = all) |
blockStreaming |
boolean | false |
Enable block streaming for responses |
chatmode |
string | "onmessage" |
Stream trigger mode: "oncall", "onmessage", "onchar" |
name |
string | - | Optional display name for the account |
email |
string | - | Bot email address |
apiKey |
string | - | Bot API key |
url / site / realm |
string | - | Zulip server URL |
allowFrom |
string[]/number[] | - | DM allowlist (Zulip sender emails, numeric user ids, or normalized names) |
groupAllowFrom |
string[]/number[] | - | Group/stream allowlist (Zulip sender emails, numeric user ids, or normalized names) |
groupPolicy |
string | "allowlist" |
Group policy: "open", "allowlist", "disabled" |
requireMention |
boolean | true |
Require @mention in streams |
oncharPrefixes |
string[] | [">", "!"] |
Trigger characters for onchar mode |
mediaMaxMb |
number | 5 |
Maximum media upload size (MB) |
textChunkLimit |
number | 4000 |
Text chunk size limit |
chunkMode |
string | "length" |
Chunking mode: "length", "newline" |
reactions |
object | enabled | Reaction indicators, e.g. { "enabled": true, "clearOnFinish": true } |
streaming |
boolean | true |
Enable receiving streaming messages |
responsePrefix |
string | - | Custom response prefix override |
| Variable | Required | Description |
|---|---|---|
ZULIP_API_KEY |
Yes | Bot API key from Zulip |
ZULIP_EMAIL |
Yes | Bot email address |
ZULIP_URL |
Yes | Zulip server URL (e.g., https://chat.example.com) |
After setup, verify the bridge works:
-
Validate config:
openclaw config validate
-
Check gateway and channel status:
openclaw status --deep
Expected channel state:
Zulip ON OK configured -
Check plugin status:
openclaw plugins doctor
-
Check logs if status is not enough. Useful markers include:
[zulip] Plugin registration complete. [default] starting provider zulip queue registered [accountId=default queueId=... lastEventId=...] -
Test direct message: send a DM from an allowed user to the bot and confirm it replies.
-
Test stream: mention
@bot-namein a monitored stream from an allowed user and confirm it replies.
If dmPolicy or groupPolicy is allowlist, make sure the sender is present in allowFrom or groupAllowFrom.
┌─────────────────────────────────────────────────────────────┐
│ OpenClaw Gateway │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Zulip Plugin (index.ts) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌─────────────┐ ┌────────────────┐ │
│ │ monitor │───▶│ client │───▶│ Zulip API │ │
│ │ (polling) │ │ (requests) │ │ (REST/WebSocket)│ │
│ └──────────────┘ └─────────────┘ └────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ reply-handler.ts │ │
│ │ - Markdown processing │ │
│ │ - Text chunking │ │
│ │ - Typing indicators │ │
│ │ - Media handling │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- monitor.ts: Event loop that polls Zulip API, maintains event queue with persistence
- client.ts: HTTP client wrapping Zulip REST API
- reply-handler.ts: Converts agent responses to Zulip format, handles chunking
- send.ts: Message delivery with security validation
- accounts.ts: Multi-account configuration resolution
Cause: The plugin was installed but "zulip" is not in plugins.allow.
Fix:
openclaw config set plugins.allow '["zulip","telegram","memory-core","exa","ollama"]'
openclaw gateway restartStatus: Harmless — the host calls registerFull twice during startup. The plugin deduplicates this.
Cause: The monitor's statusSink({ running: true }) wasn't called at the start of the monitor loop, so the host thinks the channel is dead.
Fix: Ensure statusSink is called at the top of the monitor function, not conditionally inside event handlers.
Cause: The manifest JSON Schema in openclaw.plugin.json was missing the streaming property that the host wizard writes.
Fix: Add "streaming": { "type": "boolean" } to both configSchema and channelConfigs.schema in the manifest.
Prefer loading this repository from a normal project directory via plugins.load.paths.
Check:
pwd
pnpm install
pnpm run check
openclaw config validate
openclaw gateway restart
openclaw status --deepThen verify your OpenClaw config points to the absolute path of this local repository.
- Restart the gateway:
openclaw gateway restart - Check that the plugin is in the extensions dir:
ls ~/.openclaw/extensions/zulip/
Ensure you cloned to a neutral directory and ran pnpm install && pnpm run build.
Verify credentials with openclaw channels add and re-enter them.
Ensure the bot is a member of the stream and it's in your streams config.
Default requires @mentions. Check your chatmode setting.
Status: Expected behavior
Problem: The first Zulip message after gateway startup takes ~5–8s to get a reply.
Root Cause: Model warmup + cold inference for the first agent run. Subsequent messages reply in ~2–4s.
Mitigation: None needed. This is normal for external model providers.
Two old skill packages exist on ClawHub under the same namespace:
@niyazmft/zulip-bridge(deprecated, redirects to canonical)@niyazmft/openclaw-zulip-bridge(deprecated, merged into zulip-bridge)
These are skill (text bundle) packages, not code plugins. The active, maintained package is @niyazmft/openclaw-zulip (this repo).
pnpm install
pnpm run checkThis runs: bootstrap → typecheck → build → smoke test → unit tests → package check
src/
├── channel.ts # Plugin entry point & channel config
├── setup-core.ts # Interactive setup wizard
├── setup-surface.ts # Setup wizard UI
├── config-schema.ts # Configuration validation
├── types.ts # Type definitions
├── zulip/
│ ├── auth.ts # Authentication utilities
│ ├── bootstrap.ts # Monitor initialization
│ ├── client.ts # Zulip API client
│ ├── dedupe-store.ts # Deduplication store
│ ├── media-utils.ts # Media processing
│ ├── monitor-helpers.ts # Logging helpers
│ ├── monitor.ts # Event polling & queue management
│ ├── policy.ts # DM/group policy logic
│ ├── polling.ts # Event polling
│ ├── probe.ts # Connection probing
│ ├── queue-manager.ts # Queue persistence
│ ├── reactions.ts # Reaction handling
│ ├── reply-handler.ts # Response processing
│ ├── send.ts # Message sending with security
│ ├── text-utils.ts # Text processing
│ ├── uploads.ts # Upload handling
│ └── accounts.ts # Multi-account config resolution
Contributions are welcome! Please read our contribution guidelines before submitting PRs.
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Run
pnpm run checkto validate - Submit a pull request
# Run all tests
pnpm test
# Run specific test file
pnpm test -- test/policy.test.tsFor security vulnerabilities, please do not open a public issue. Contact the maintainer directly through GitHub or email.
MIT License - see LICENSE file for details.