Found lark-channel-bridge, which connects Feishu directly to your local Claude Code CLI. Tried it, works well, documenting the process and pitfalls.

Getting It Running

npm i -g lark-channel-bridge
lark-channel-bridge run

First run outputs a QR code — scan it with Feishu, and it automatically creates a Feishu app. Credentials land in ~/.lark-channel/. After that, just send a message in Feishu: no @ needed in DMs, @ the bot in groups.

Want it running in the background: lark-channel-bridge start (supports macOS launchd / Linux systemd / Windows Task Scheduler).

What It Does

Simply put, bridge does one thing: forwards Feishu messages to your local claude process, then forwards claude's replies back to Feishu.

Feishu App ←→ Feishu Open Platform (WebSocket) ←→ bridge ←→ Claude Code CLI

Key point: bridge runs locally, not in the cloud. So claude can directly access your filesystem, Git repos, and local tools — exactly the same as using it in the terminal. Tradeoff: your computer needs to be on.

How bridge ↔ claude Communicate

This is the most elegant part:

When bridge receives a Feishu message, it spawn("claude", ["-p", prompt, "--output-format", "stream-json", "--resume", sessionId, ...]) to start a claude subprocess. claude outputs JSON line by line via stdout (JSONL) — each line is a text segment or a tool call. bridge splits on \n, JSON.parses each line, and translates it into a Feishu card patch update — this is the "streaming card": claude updates the same card as it generates, tool calls show up in real-time.

There's no custom protocol between bridge and claude — just OS parent-child process + anonymous pipe. "Streaming" relies entirely on claude CLI's built-in stream-json output mode; bridge just reads lines, parses, translates — clean and simple. When a turn completes the process exits naturally; next message spawns a new one.

How Sessions Are Maintained

bridge itself doesn't store session history. sessionId is extracted from claude's {type:"system", subtype:"init"} event on first spawn; history lives in claude's own ~/.claude/projects/.../<sid>.jsonl. bridge only maintains a thin chatId → sessionId dictionary.

So: concurrent claude processes ≈ concurrent active turns. Each turn finishes, process exits, OS reclaims the PID — no leaks.

The entire bridge is roughly 3-5k lines of TypeScript, delegating all the heavy lifting to claude CLI — that's why it's "light".

Useful Features

Since the underlying layer is Claude Code, your locally configured skills, workflows, and CLAUDE.md rules all work as-is. My blog has /check-post for format validation and /fetch-zhihu for scraping Zhihu articles — now I can send /fetch-zhihu https://zhuanlan.zhihu.com/p/xxx from my phone, same as sitting at my computer.

Beyond the basic "run claude" capability, bridge does a few things that make the experience smoother. Streaming output, preemption, and per-chat sessions are tied together — what you see in Feishu is a single card that updates in real-time; sending a new message interrupts the old task, rapid-fire messages get merged; DMs and different groups don't interfere, and each topic in a topic group is independent too.

Other bits: /ws save /ws use to switch project directories, images and files can be sent directly (bridge downloads and feeds the path to claude), /status /new /cd /stop /doctor for common slash commands (other /xxx passes through to claude). Interactive cards support CardKit 2.0 — Claude can send cards with buttons and forms, clicks callback to the same session. Multi-bot collaboration works too, pull several AI bots into a group and divide work via @ — doesn't @ other bots by default to avoid ping-pong loops.

Pitfalls

Windows Paths

My dev environment is Windows; some path handling in bridge assumes Unix style. Working directory passing uses D:\homepage on Windows while bridge expects /d/homepage. Fix: explicitly specify the working directory at startup, make sure project path is set correctly.

Bots in Groups Only Receive @ Messages

Feishu platform limitation, not a bug. Bots only receive group messages when genuinely @'d (structured mention). Typing "@botname" in plain text doesn't work. Add the bot to a project group, and the team needs to get in the habit of @'ing it.

Authorization and Process Lifecycle

bridge supports Claude Code using user identity for Feishu API calls, which requires OAuth. But OAuth's device flow generates a verification link — in a group, whoever clicks first gets the token, binding it to the wrong identity, so authorization can only be done in private chats. bridge auto-detects chat type and refuses lark-cli auth login in groups.

Another process-related gotcha: bridge reclaims the Claude Code subprocess after a reply finishes. If you start lark-cli auth login with run_in_background: true, the background process dies with it. Authorization must run in foreground blocking mode — first --no-wait to get the verification link, then --device-code to block and wait for the user to complete.

Security

bridge controls access via Feishu identity verification — only messages from people in designated chats are processed. Claude's permissions are the same as in the terminal, constrained by CLAUDE.md and Claude Code's own permission confirmation. Code execution happens entirely on your machine; nothing is uploaded to additional third-party services.

Verdict

Daily routine now: message the bot from Feishu when I'm out — fix bugs, write articles, deploy the site. Back at the computer, all changes are already in local files. Seamless multi-device handoff, conversation context persists.

Set it up once, use it for a long time.