npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claude-plugin-zalo

v0.0.4

Published

Zalo channels for Claude Code — receive and reply to Zalo messages live in your Claude session

Downloads

205

Readme

claude-plugin-zalo

A Claude Code MCP plugin that bridges Zalo into your Claude sessions. Messages from approved senders appear live in your interactive session as channel events, and Claude replies under your own identity — this is your personal Zalo account, driven over a WebSocket by zca-js, not a bot.

Features

  • Live channel events — no polling; allowed senders' messages render in the session as they arrive
  • Pairing-code access control — strangers get a code; you approve with /zalo:access pair <code>
  • Allowlist lockdowndmPolicy: allowlist silently drops anyone you haven't approved
  • Group mention-gating — opt groups in, optionally only when your account is @mentioned
  • Reply, react, and attachments — chunked text replies, emoji/reaction codes, and inbound photo/file download
  • Permission relay — Claude Code permission prompts are forwarded to your DM; reply yes <id> / no <id>
  • Last session wins — starting a new Claude session takes over the Zalo connection from the old one

Prerequisites

  • Bun ≥ 1.2 — must be on your PATH; works on Windows, Linux, and macOS
  • A Zalo personal account

Install

# Add the plugin source (one-time)
claude plugin marketplace add imrim12/claude-plugin-zalo

# Install
claude plugin install zalo@imrim12

Verify it loaded:

claude plugin list   # should show: zalo  ✔ loaded

The MCP server runs straight from npm via bunx claude-plugin-zalo — Bun fetches the package and its dependencies on first launch and caches them, so there's no manual install step and nothing OS-specific to configure. State lives under your home directory (~/.claude/channels/zalo), resolved cross-platform, independent of where Claude Code is launched from.

Enable inbound delivery (required)

Claude Code only renders notifications/claude/channel events from plugins on Anthropic's approved channels allowlist. This plugin is not on it, so inbound messages are silently dropped unless you launch the session with the development-channels flag:

claude --dangerously-load-development-channels plugin:zalo@imrim12

# Or YOLO mode

claude --dangerously-load-development-channels plugin:zalo@imrim12 --dangerously-skip-permissions

A confirmation dialog appears at startup — accept it. Without this flag, everything else still works (QR login, pairing auto-replies, outbound reply), which makes the failure look like a plugin bug: the sender sees the typing indicator but the message never reaches your session.

To confirm you're hitting this, check the newest file in Claude Code's MCP log directory for your project (%LOCALAPPDATA%\claude-cli-nodejs\Cache\<project>\mcp-logs-plugin-zalo-zalo\ on Windows) for:

Channel notifications skipped: plugin zalo@imrim12 is not on the approved channels allowlist

Quick start

1. Log in (personal account)

  1. Run /zalo:auth (or call the zalo_login tool).
  2. Scan the QR code at the path /zalo:auth reports (qr-login.png in the state directory) with the Zalo mobile app (More → QR scan).
  3. Confirm on your phone. Credentials are saved automatically — no re-scan needed on restart.

2. Pair your other account

The default DM policy is pairing: when an unknown sender DMs your account, the plugin auto-replies with a 6-character code. Approve them in your terminal:

/zalo:access pair <code>

They get a "Paired!" DM, and from then on their messages appear live in your session (provided inbound delivery is enabled — see above).

3. Lock it down

Once everyone you want is paired, switch to a hard allowlist so strangers are dropped silently:

/zalo:access policy allowlist

Manage everything else with /zalo:accessallow <id>, remove <id>, deny <code>, group add <threadId> [--no-mention] [--allow id1,id2], group rm <threadId>.

MCP tools

| Tool | Description | |---|---| | reply | Reply to an inbound channel message (allowlist-gated, chunks long texts, optional quote via reply_to) | | react | React to a received message — common emoji or raw zca reaction codes (allowlist-gated) | | download_attachment | Fetch a received attachment (document/voice/video/…) to the local inbox by message_id | | zalo_login | Start the QR login flow; writes the QR image and returns its path |

Access control has no MCP tools — it's managed entirely by the /zalo:access skill editing access.json in the state directory. This keeps access mutations out of reach of prompt injection arriving through channel messages.

Skills

| Skill | Purpose | |---|---| | /zalo:auth | QR login to your personal account | | /zalo:configure | Orient on login + access state; drive toward an allowlist lockdown | | /zalo:access | Approve pairings, edit the allowlist, set DM/group policy | | /zalo:status | Diagnose connection and inbound-delivery issues from state files and logs |

Environment variables

| Variable | Required | Purpose | |---|---|---| | ZALO_STATE_DIR | No | Force a specific state directory, overriding the resolution below | | ZALO_ACCESS_MODE=static | No | Snapshot access at boot; never re-read or written (pairing downgrades to allowlist) |

State directory

State lives under a .claude/channels/zalo directory, split into two scopes:

Authentication (account-global) — always ~/.claude/channels/zalo:

  • credentials.json — your Zalo session, and qr-login.png — the QR login image. The account is global, so one QR scan works across every project. ZALO_STATE_DIR overrides this location.

Chat state (per session) — resolved in this order:

  1. ZALO_STATE_DIR if set — used verbatim.
  2. <project>/.claude/channels/zalo — when the project you launched Claude Code in already contains a .claude/ folder. This keeps Zalo chat state scoped to that project. The plugin only adopts an existing .claude/; it never creates one.
  3. ~/.claude/channels/zalo — the default, used when the project has no .claude/.

The channel server (via the CLAUDE_PROJECT_DIR it's given) and the /zalo:* skills resolve both scopes the same way, so they always read and write the same files.

State files

credentials.json and qr-login.png are under the user-root dir; everything else is under the resolved chat-state directory (mode 0600, atomic writes):

  • credentials.json — Zalo session: imei, userAgent, cookie jar (rotated and re-saved on every login) — user-root
  • qr-login.png — last QR login code — user-root
  • access.jsondmPolicy, allowFrom, groups, pending pairings, delivery/UX config
  • approved/<senderId> — touch-files dropped by /zalo:access pair; the server polls, DMs "Paired!", and removes them
  • inbox/ — downloaded attachment bytes
  • bot.pid — current connection owner (last session wins)

Architecture

server.ts is a thin entry shim; the implementation lives in src/ (entry point src/main.ts). Each module has one responsibility:

| Module | Responsibility | |---|---| | main.ts | Wires everything together; owns process lifecycle (PID takeover, shutdown, orphan watchdog) | | access.ts | access.json types + read/write, static-mode snapshot, outbound chat gate | | gate.ts | The fail-secure inbound gate: pairing / allowlist / group + mention policy | | session.ts | Zalo login/listener lifecycle: cookie re-login with backoff, QR bootstrap, kick stand-down | | inbound.ts | Inbound pipeline: self-filter → cache → gate → pairing auto-reply or channel notification | | tools.ts | The 4 MCP tools | | permissions.ts | Permission-request relay to DMs + yes/no <id> reply intercept | | attachments.ts | Attachment kind/href mapping, inbox downloads, sender-name sanitizing | | mcp.ts, approvals.ts, credentials.ts, message-cache.ts, reactions.ts, chunk.ts, pidfile.ts, paths.ts, log.ts | MCP server instance, approval polling, credential persistence, quote/react cache, emoji→reaction codes, text chunking, PID file, path constants, stderr logging |

Message flow: Zalo WebSocket → api.listener → self filter → gate()notifications/claude/channel → renders in your session as <channel source="zalo" ...>.

Takeover: one process owns the Zalo connection (bot.pid). Starting a second Claude session kills the first server and takes over.

Kick stand-down: if another Zalo session (phone/browser/second instance) takes the listener slot, the server stands down instead of fighting for it — re-login would churn the cookie. Tools error clearly until you run zalo_login or restart.

Development

pnpm typecheck   # tsc --noEmit
pnpm lint        # oxlint --deny-warnings
bun test         # MCP protocol tests (spawned against a temp state dir)
pnpm start       # bun server.ts

Publishing

The plugin's .mcp.json launches the server with bunx claude-plugin-zalo, so the package must be on npm for installs to resolve. To publish a new version:

npm version <patch|minor|major>   # also bump .claude-plugin/plugin.json to match
npm publish                       # .npmignore controls what ships

npm pack --dry-run previews the tarball — it should contain src/, server.ts, skills/, .claude-plugin/, .mcp.json, README.md, and LICENSE, and nothing else.