Skip to content

Latest commit

 

History

84 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Feishu Channel for Claude Code

npm version license

A Feishu (Lark) channel plugin for Claude Code, built on Claude Code's native Channel interface. Send messages to a Feishu bot and interact with Claude — right in your chat, with WebSocket persistent connection mode requiring no public HTTPS endpoint.

Claude Code is the primary way in — you install the plugin, run claude-feishu, and every message from Feishu reaches a live Claude Code instance. But the router behind it isn't limited to Claude: each Feishu group can instead be pointed at an OpenCode session, and a group can even fall back to OpenCode automatically when its Claude Code worker isn't connected. Same bot, same chat, your choice of backend per group.

npx feishuchannel-for-claudecode   # one-command install

Multi-Group Router — One Bot, Many Projects (and Backends)

The killer feature: route different Feishu groups to different Claude Code instances — or to OpenCode — each working in its own project directory. A single Feishu bot serves your entire team — each group gets its own isolated agent with full project context, and the backend is a per-group config choice, not a code change.

                             ┌─ Claude Code (project-a)
Feishu Bot ──→ Router ───────┤─ Claude Code (project-b)
           (single WebSocket)├─ OpenCode      (project-c)
                             └─ Claude Code (project-d), fallback → OpenCode
                                 ▲ Claude Code workers auto-connect via Unix socket

How it works:

  • The router holds the single Feishu WebSocket connection and routes messages by chat_id → project → backend (claude | opencode)
  • For Claude Code, each worker (server.ts) runs inside a Claude Code instance, registered by its working directory, and connects to the router over a Unix socket
  • For OpenCode, the router talks directly to an opencode serve HTTP/SSE API — no worker process, no manual per-project startup
  • The first claude-feishu launch auto-spawns the router — no manual setup needed
  • When all Claude Code workers disconnect, the router auto-shuts down after a grace period — this check only counts Claude Code workers, so if you run OpenCode-only groups with no Claude Code project ever connected, run the router as a persistent service instead (see Running the Router as a Service)

Zero-config startup — just run claude-feishu in each project directory:

cd /path/to/project-a && claude-feishu   # spawns router + connects as worker
cd /path/to/project-b && claude-feishu   # connects to existing router
cd /path/to/project-c && claude-feishu   # connects to existing router

Map Feishu groups to project directories in ~/.claude/channels/feishu/access.json:

{
  "groups": {
    "oc_groupA": { "workdir": "/path/to/project-a", ... },
    "oc_groupB": { "workdir": "/path/to/project-b", ... }
  },
  "defaultWorkdir": "/path/to/default-project"  // DMs route here
}

See Multi-Group Router Setup for full configuration details, or OpenCode Backend to route specific chats to OpenCode instead of Claude Code.

Features

  • Multi-group routing — One Feishu bot serves multiple Claude Code instances, each in its own project
  • Auto-managed router — Router spawns on first launch, shuts down when all workers disconnect
  • OpenCode backend (opt-in per group/DM) — Route specific chats to OpenCode instead of Claude Code, with sessions persisted in SQLite across router restarts
  • Claude → OpenCode fallback (opt-in) — If no Claude Code worker is connected, or Claude Code just hit its usage rate limit, hand that message to OpenCode instead of dropping it (pre-execution cases only, see Fallback to OpenCode)
  • Direct messages — Chat with Claude through Feishu DMs
  • Group chats — Add the bot to group chats with @mention support
  • Access control — Pairing-based onboarding, allowlists, and per-group policies
  • Confirm cards — Interactive confirmation cards for risky actions
  • Permission cards — Interactive approve/deny cards for tool permission requests
  • Unanswered reminders — Auto-nudges Claude if a message goes unanswered for 30+ minutes (up to 3 times, escalating intervals)
  • Attachments — Send and receive files and images
  • Reactions — Configurable emoji reactions on message receipt
  • Smart connection — Only connects when launched as a channel, skipping unnecessary connections
  • Graceful shutdown — Detects parent process exit via ppid polling, preventing orphaned processes

Prerequisites

  • Bun runtime
  • Claude Code CLI installed
  • A Feishu (or Lark) workspace with admin access to create apps

Quick Start

1. Create a Feishu App

  1. Go to Feishu Open Platform (or Lark Open Platform for international)

  2. Create a Custom App (enterprise internal app)

  3. Note the App ID (cli_...) and App Secret

  4. Under Events & Callbacks, configure two separate tabs:

    Event Configuration tab:

    • Switch connection method to Using persistent connection (WebSocket mode) at the top of the page
    • Add event: im.message.receive_v1

    Callback Configuration tab:

    • Also switch to Using persistent connection (WebSocket mode)
    • Add callback: card.action.trigger (Card interaction callback) — required for confirm card buttons to work
  5. Under Permissions & Scopes, add:

    Permission Purpose
    im:message Send messages
    im:message.receive_v1 Receive messages
    im:message.p2p_msg:readonly Read DM messages
    im:message.group_at_msg:readonly Read group @mentions
    im:chat:readonly Read chat metadata
    im:resource Download attachments
  6. Publish the app version so permissions take effect

2. Install the Plugin

One command:

npx feishuchannel-for-claudecode

This clones the repo, installs dependencies, registers the Claude Code plugin, and creates the claude-feishu shortcut — all automatically.

Manual installation
git clone https://github.com/phxwang/feishuchannel-for-claudecode.git
cd feishuchannel-for-claudecode
bun install
claude plugin marketplace add .
claude plugin install feishu@feishu-local

3. Start Claude Code with the Feishu Channel

claude-feishu

On subsequent launches, claude-feishu automatically resumes the session named after the current directory (e.g., a session named ccmyproject is matched in the myproject/ directory). If no matching session is found, an interactive session picker opens.

Or use the full command:

claude --dangerously-load-development-channels plugin:feishu@feishu-local

4. Configure Credentials

In your Claude Code terminal:

/feishu:auth cli_YOUR_APP_ID YOUR_APP_SECRET

Credentials are stored in ~/.claude/channels/feishu/.env (mode 600).

5. Pair Your Account

  1. Open Feishu and search for your bot by app name

  2. Send any message to the bot

  3. The bot replies with a pairing code

  4. In Claude Code, run:

    /feishu:access pair <code>
    
  5. The bot confirms: "Paired! Say hi to Claude."

You're ready — send messages to the bot and Claude will respond.

Multi-Group Router Setup

The steps below start with the zero-config ad-hoc flow (fine for quick local testing), but if you're relying on this daily — or running any group on OpenCode, which has no worker to auto-spawn the router — install the router as a persistent service first (step 3) and skip step 2's manual spawn-ordering entirely: each claude-feishu just connects to the already-running router.

1. Configure Group Workdirs

Add workdir to each group in ~/.claude/channels/feishu/access.json:

{
  "groups": {
    "oc_groupA": {
      "requireMention": true,
      "allowFrom": [],
      "workdir": "/path/to/project-a"
    },
    "oc_groupB": {
      "requireMention": true,
      "allowFrom": [],
      "workdir": "/path/to/project-b"
    }
  },
  "defaultWorkdir": "/path/to/default-project"  // DMs route here
}

2. Start Claude Code Instances

In separate terminals, start Claude in each project directory:

cd /path/to/project-a
claude-feishu

cd /path/to/project-b
claude-feishu

The first instance auto-spawns the router (unless a router is already running as a service, in which case it just connects). Subsequent instances connect as workers. The router matches incoming messages by chat_id → project → backend (Claude Code worker, or OpenCode directly).

3. Running the Router as a Service (recommended for anything long-running)

The auto-spawn behavior above is convenient for quick starts, but for a machine that stays up (a dev box, a server), run the router as a supervised background service instead — it survives crashes and reboots without depending on any particular claude-feishu instance staying alive to relaunch it. Ad-hoc bun router.ts or a tmux pane both work for testing, but once a service is installed, don't also start the router manuallyrouter-lock.ts prevents two routers from serving traffic at once, so a manually-started process will just exit immediately (or fight with the service over the singleton lock).

macOS (launchd):

1. Install — pick a label (com.<you>.feishuchannel-router), then write the plist. Fill in the absolute paths for your setup: which bun for the bun binary, the repo's absolute path, and wherever you want logs written.

mkdir -p ~/Library/LaunchAgents ~/logs   # or wherever you want StandardOut/ErrorPath to live

cat > ~/Library/LaunchAgents/com.you.feishuchannel-router.plist <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.you.feishuchannel-router</string>
  <key>ProgramArguments</key>
  <array>
    <string>/path/to/bun</string>
    <string>/path/to/feishuchannel/router.ts</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>10</integer>
  <key>WorkingDirectory</key><string>/path/to/feishuchannel</string>
  <key>StandardOutPath</key><string>/path/to/logs/router.stdout.log</string>
  <key>StandardErrorPath</key><string>/path/to/logs/router.stderr.log</string>
</dict>
</plist>
PLIST

launchctl load -w ~/Library/LaunchAgents/com.you.feishuchannel-router.plist

load -w starts it immediately and registers it to start on every future login/reboot too — no separate "enable" step needed.

2. Verify it's actually running:

launchctl print gui/$(id -u)/com.you.feishuchannel-router | head -5
# state = running    <- what you want to see

3. Restart after pulling a code update (KeepAlive only relaunches it on an unexpected exit, not on demand):

launchctl kickstart -k gui/$(id -u)/com.you.feishuchannel-router

4. Uninstall, if you go back to ad-hoc/claude-feishu-auto-spawn mode:

launchctl unload -w ~/Library/LaunchAgents/com.you.feishuchannel-router.plist
rm ~/Library/LaunchAgents/com.you.feishuchannel-router.plist

Router-internal status (connected workers, regardless of how the process was started):

kill -USR1 $(pgrep -f 'bun.*router.ts')
tail -10 ~/.claude/channels/feishu/router-debug.log

OpenCode Backend

The router can also dispatch a group (or DM) to OpenCode instead of Claude Code — useful as an alternative backend for specific projects. This is opt-in per group/DM and has zero effect on anything not explicitly configured.

Prerequisites: a running opencode serve instance, bound to localhost:

opencode serve --hostname 127.0.0.1 --port 4096

(Run it as a persistent daemon — e.g. via launchd/systemd — independent of the router's own lifecycle.)

1. Declare backends and projects in agents.yaml

Create ~/.claude/channels/feishu/agents.yaml (optional — if this file is absent, everything stays Claude-only with unchanged behavior):

version: 1
defaults:
  agent:
    primary: claude
    fallback: null
agents:
  claude:
    type: claude-channel
    socket: /Users/you/.claude/channels/feishu/router.sock
    health:
      workerTtlSeconds: 15
  opencode:
    type: opencode
    baseUrl: http://127.0.0.1:4096
    requestTimeoutSeconds: 30
    taskTimeoutSeconds: 600
    maxConcurrency: 4
projects:
  my-project:
    workdir: /path/to/my-project
    allowedAgents: [claude, opencode]
fallback:
  enabled: true    # see "Fallback to OpenCode" below for exactly what this does
  triggerOn: []              # reserved for a future richer fallback state machine, currently unused
  neverTriggerOn: [permission_denied, user_abort, invalid_request, tool_failure, partial_execution]  # reserved, currently unused
  maxAttempts: 1              # reserved, currently unused
  cooldownSeconds: 300         # reserved, currently unused
  notifyUser: true            # reserved, currently unused

agents.yaml never decides which project a chat maps to — that's still access.json's job (see below). It only declares which backends exist and which agents each project is allowed to use. Only fallback.enabled is actually read by the router today — the other fallback.* fields are validated but not yet consulted anywhere in dispatch; they're placeholders for a future richer fallback policy.

2. Opt a group or DM into OpenCode via access.json

access.json is the single source of truth for chat → project/agent mapping. Add an optional agent field to any group entry (or defaultAgent at the top level for DMs):

{
  "groups": {
    "oc_groupA": {
      "workdir": "/path/to/my-project",
      "agent": { "primary": "opencode", "fallback": null }
    }
  }
}

Any group/DM without an explicit agent field stays on Claude, regardless of what's declared in agents.yaml. A request for an agent not in that project's allowedAgents (or a workdir not declared in agents.yaml at all) silently falls back to Claude-only — check router-debug.log if a group doesn't behave as expected.

Fallback to OpenCode

Set agent.fallback: "opencode" on a group/DM (alongside primary: "claude") and agents.yaml's fallback.enabled: true to let that chat fail over to OpenCode. This covers two narrow, safe cases where the message provably never reached Claude, so there's no partial work to reconcile:

  • No Claude Code worker connected for that project at all.
  • Claude Code is rate-limited. The worker tails its own transcript for the error:"rate_limit" event Claude Code writes when it hits a usage limit, and tells the router it's unavailable until Claude's own stated reset time (rate-limit-reset.ts parses text like "...resets 9:30pm (Asia/Singapore)" — the IANA zone Claude reports is used directly, so this is correct even across timezones/DST). If that text can't be parsed, it falls back to a fixed 30-minute cooldown (worker-link.ts's DEFAULT_DEGRADED_MS), refreshed on each new rate_limit event. The router skips routing to that worker entirely until the window ends.

In either case the router replies with a one-line notice and hands the task to OpenCode instead of showing the usual "no active session" error.

It does not cover a Claude worker accepting a task and then failing partway through — that needs real task-state tracking (the design doc's full fallback state machine, routing/task-machine.ts), which exists as pure logic but isn't wired into the live dispatch path yet.

Notes:

  • OpenCode sessions are sticky per conversation and persisted in SQLite (router.db) — they survive a router restart.
  • Permission requests from OpenCode's tool calls surface as an interactive Feishu approve/deny card, same as Claude's.
  • Attachments aren't supported on the OpenCode path yet.
  • OpenCode replies are chunked at a fixed 4096 chars — they don't honor a group's textChunkLimit override (that setting only applies to Claude Code replies today).

Access Management

All access commands are run in your Claude Code terminal via /feishu:access.

Check Status

/feishu:access

DM Policies

Policy Behavior
pairing (default) Unknown users get a pairing code to approve
allowlist Unknown users are silently dropped
disabled All messages dropped
/feishu:access policy allowlist

Tip: Once all your users are paired, switch to allowlist to prevent unsolicited pairing requests.

Manage Users

# Approve a pairing
/feishu:access pair <code>

# Deny a pairing
/feishu:access deny <code>

# Manually allow a user by open_id
/feishu:access allow ou_xxxxxxxxxxxxxxxxxxxx

# Remove a user
/feishu:access remove ou_xxxxxxxxxxxxxxxxxxxx

Group Chats

Groups are off by default. The bot must be added to the group by a group admin first.

# Enable a group (responds on @mention only)
/feishu:access group add oc_xxxxxxxxxxxxxxxxxxxx

# Respond to all messages (no @mention needed)
/feishu:access group add oc_xxxxxxxxxxxxxxxxxxxx --no-mention

# Restrict to specific users within the group
/feishu:access group add oc_xxxxxxxxxxxxxxxxxxxx --allow ou_id1,ou_id2

# Remove a group
/feishu:access group rm oc_xxxxxxxxxxxxxxxxxxxx

Delivery Settings

# React to received messages with an emoji (default: Get)
/feishu:access set ackReaction Get

# Set max characters per message chunk
/feishu:access set textChunkLimit 4096

# Custom mention patterns for group chats
/feishu:access set mentionPatterns ["@claude","@assistant"]

File Layout

~/.claude/channels/feishu/
├── .env              # App credentials (FEISHU_APP_ID, FEISHU_APP_SECRET)
├── access.json       # Access control + chat->project/agent mapping (auto-managed)
├── agents.yaml        # Optional backend/infra config for OpenCode (see OpenCode Backend)
├── approved/         # Pairing approval signals (transient)
├── inbox/            # Downloaded attachments
├── debug.log         # Worker (server.ts) debug log
├── router-debug.log  # Router debug log
├── router.sock       # Unix socket workers connect to
├── router.db         # SQLite: conversation bindings, permission callbacks, processed-event dedup
└── router.lock/       # Router singleton lock (prevents duplicate router processes)

Environment Variables

Variable Required Description
FEISHU_APP_ID Yes Feishu app ID (cli_...)
FEISHU_APP_SECRET Yes Feishu app secret
FEISHU_ENCRYPT_KEY No Event payload encryption key
FEISHU_ACCESS_MODE No Set to static to disable pairing
FEISHU_STATE_DIR No Override state directory path (default ~/.claude/channels/feishu)
FEISHU_PROJECTS_ROOT No Root directory agents.yaml project workdirs must resolve under (default ~/Projects)

How It Works

Smart Connection

The plugin detects whether it's running under a Feishu channel Claude instance by walking up the process tree and checking for --dangerously-load-development-channels with feishu in the ancestor's command line. Non-channel Claude instances (e.g., regular claude or claude --channels plugin:discord@...) skip the Feishu WebSocket connection entirely, keeping the MCP tools available without unnecessary remote connections.

Orphan Protection

When the parent Claude process exits, the plugin detects the ppid change within 2 seconds and shuts down gracefully. This prevents orphaned bun server.ts processes from consuming 100% CPU — a workaround for Bun not reliably firing stdin end/close events on broken unix domain sockets.

Testing

bun test

147 tests across 10 files. server.test.ts covers access control (gate logic), text chunking, mention detection, permission reply parsing, confirm code generation, and chat authorization. routing/*.test.ts covers config loading/validation, route resolution (access.json + agents.yaml merge, sanitization), conversation bindings, SQLite storage, and the (currently-unwired) fallback state machine. adapters/**/*.test.ts covers the OpenCode HTTP/SSE client (including the event-stream-never-closes edge case) and the Claude adapter's capability boundaries.

Security

  • Credentials are stored with chmod 600 — only the owner can read them
  • Pairing codes expire after 1 hour
  • After 2 unapproved messages, senders are silently dropped until the code expires
  • Access mutations can only be made from the Claude Code terminal — never from channel messages (prompt injection protection)
  • Group chats require explicit opt-in per group

License

MIT

About

Feishu channel plugin for Claude Code — route Feishu groups to isolated Claude Code instances (or OpenCode) via a single bot, with an auto-managed router, Claude→OpenCode fallback, access control, and WebSocket persistent connection.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages