Feishu Integration
September 21, 2026 · View on GitHub
Talk to your Kiro Crew agent from Feishu (Lark / 飞书) — through a Feishu custom app bot. Create the app in the Feishu developer console, drop in two values, and you're chatting.
Like Telegram and WeCom, the connection is outbound-only: Kiro Crew opens a long-connection WebSocket to Feishu, so there's no callback URL, public hostname, or open port to manage.
Optional dependency. The Feishu long connection is the one channel that is only specified through a vendor SDK (handshake, tenant-token refresh, and frame envelope), so it needs
lark-oapi. It is not part of core, so install it directly into the environment that runs the gateway:pip install 'lark-oapi>=1.4,<2'. Without it the gateway logs a line and skips the channel — nothing else is affected.
Quick start
You'll need a running gateway (kirocrew gateway), lark-oapi installed, and
access to https://open.feishu.cn/app (or https://open.larksuite.com/app for
Lark).
- Create a custom app — in the developer console, create an app and note its App ID and App Secret from Credentials & Basic Info.
- Add the bot capability — Add features → Bot.
- Grant permissions — under Permissions & Scopes, add
im:message.p2p_msg:readonly(receive DMs) andim:message:send_as_bot(reply). If you enable group chats, also addim:message.group_at_msg.include_bot:readonly(receive group messages that @-mention the bot). The broaderim:messagegrant does not replace the explicit p2p event scope in the current Feishu console. - Use long connection — under Events & Callbacks, choose
Long connection (not a request URL), then subscribe to
im.message.receive_v1. - Publish the app so your tenant can install it.
- Find your open_id — the console's API Explorer, or any inbound message
in the gateway log, shows the sender's
open_id(it starts withou_). - Paste it into Settings — open Settings → Messaging Channels → Feishu, put the
App ID and App Secret in their fields, add your
open_idto the allow-list, turn the channel on, and save. Saving writes the two secrets to~/.kiro/crew/.env(owner-only,0600) and the rest toconfig.json, so there is nothing left to hand-edit. - Restart:
kirocrew restart
DM the bot in Feishu and it answers. The panel's badge tells you where you are: Needs setup until both credentials are stored, Not connected once they are but the receiver is not running, Connected when it is.
Prefer to edit the files directly?
The panel is a front end over the same two files, so this is equivalent:
# ~/.kiro/crew/.env
FEISHU_APP_ID=cli_xxxxxxxxxxxx
FEISHU_APP_SECRET=your-app-secret
// ~/.kiro/crew/config.json
"feishu": {
"enabled": true,
"allowed_open_ids": ["ou_xxxxxxxxxxxxxxxx"]
}
Config writes from the panel are loopback-only — a remote or tunnelled dashboard session gets a read-only view, because widening who may reach the agent is not something a forwarded request should be able to do. On such a session, editing the files on the gateway's own machine is the way in.
Access control
Deny-by-default, in both directions:
allowed_open_idsis the allow-list. An empty list authorises nobody — the bot stays reachable on the Feishu side but rejects every message. This is deliberate: publishing an app must never mean an open door.- Group chats need two switches. A group message is served only when
allow_groupistrueand the group'schat_idis inallowed_group_ids. Either one alone denies. - A group has its own conversation. A group turn is keyed by the group's
chat_idunder a non-direct chat type, so an allow-listed user writing in a group never resumes their private DM session — their DM history cannot become context for a reply the whole room reads. This holds undermessaging.dm_scope: unifiedtoo, where a DM key deliberately collapses into a cross-channel bucket and a group key deliberately does not. - Only two chat types are served. A DM (
p2p) and a group (group) are named explicitly; a message whose chat type is absent or unrecognised is denied rather than assumed to be a DM, so a context whose authorisation was never evaluated can never run a turn. - Every denial is audited. Rejected inbound messages write a SEL audit
record with
source: feishu, so an unexpected sender shows up inkirocrew security posturerather than vanishing. - Feishu itself only delivers a group message to the bot when it is @-mentioned, so a bot in a busy group does not see unrelated chatter.
- Mentions reach the agent as names. Feishu puts opaque placeholders
(
@_user_1) in the message text and the display names in a separatementionslist, so the two are rejoined before the agent sees the prompt: "ask @Alice to review" stays intact rather than becoming "ask to review".@_allbecomes@all. A message that is nothing but mentions carries no instruction and is ignored, so a bare@botdoes not start an empty turn. - Slash commands work in a group too. A group message has to mention the
bot, so it arrives as
@FeishuBot /new. That single leading mention is removed before matching, so/new,/resetand/compactstill intercept there instead of being sent to the agent as a prompt. Only a bare command intercepts, and a message naming anyone else is never a command:@FeishuBot please run /new laterand@FeishuBot /new @Aliceare both ordinary prompts. That second case is deliberate — reading it as/newwould reset the conversation on a message you addressed to a colleague, and that is not recoverable, so anything ambiguous is treated as a prompt.
Settings reference
| Key | Default | What it does |
|---|---|---|
feishu.enabled | false | Top-level switch. Also needs both env vars. |
feishu.allowed_open_ids | [] | Who may DM the bot. Empty = nobody. |
feishu.allow_group | false | Serve group chats at all. |
feishu.allowed_group_ids | [] | Which groups, when allow_group is on. |
feishu.soft_threshold_pct | 80 | Context % that prompts you to /compact or /new. |
feishu.hard_threshold_pct | 95 | Context % that forces a compaction. |
feishu.session_folder | "" | Sidebar folder for sessions that start here. Empty = unfiled. |
Everything above except hard_threshold_pct is editable in Settings →
Channels → Feishu; that one is file-only, because the panel exposes the soft
threshold as the single number worth tuning and keeps the hard ceiling as a
safety net.
Credentials come from the environment only (FEISHU_APP_ID,
FEISHU_APP_SECRET), matching the console's own naming — they are never written
into config.json. The panel stores them in .env and afterwards shows only a
masked preview: a saved secret can be replaced or cleared, never read back.
Commands
| Command | Effect |
|---|---|
/new (or /reset) | Start a fresh session; the old context is dropped. |
/compact | Compact the current context in place. |
Anything else is a prompt.
Limits
Feishu v1 is deliberately single-shot: the agent's answer is buffered and sent
as one reply when the turn completes, rather than streamed. There are no
interactive buttons, so when the agent offers a choice, the [OPTIONS: …]
trailer arrives as a numbered list and you answer by typing one — which is an
ordinary message, so nothing extra has to be configured.
Known gaps, all follow-up work rather than defects:
- No streaming. Feishu supports
PATCH /im/v1/messages/{id}, so edit-in-place streaming is a natural next step. - Text only. Non-text messages (images, files, audio) are ignored inbound.
- Replies only. The bot answers an inbound message and cannot start a conversation, so it is not a proactive-notification target.
- The panel configures the channel; it does not install it.
lark-oapiis an optional extra, and runningpipfrom a dashboard action would be a new way to execute code in the gateway's own environment. When the SDK is missing, Settings reports whether installation is supported for this gateway interpreter and shows its exact install command only when it is; you run that command yourself. - Credentials are not verified on save. A REST tenant-token probe would have
to pick a domain (
open.feishu.cnoropen.larksuite.com) and would report a false failure for whichever tenant it guessed wrong, so the panel stores what you give it. Nothing is lost: the badge tracks the receiver, and Feishu drops a refused app within seconds of a restart, which turns into Not connected with the reason attached. - No auto-reconnect beyond the SDK's own.
lark-oapireconnects internally; if it gives up, the gateway logs that the receiver is down and you restart.
Bot doesn’t receive direct messages
- Confirm
im:message.p2p_msg:readonlyis granted. A greenim:message.group_at_msg.include_bot:readonlygrant covers group @-mentions, not private messages. - Publish a new app version after changing permissions; a draft permission does not affect the installed bot.
- Confirm
im.message.receive_v1appears in the subscribed-event list, not only that long connection is selected.
How it fits together
The channel is a thin transport over the shared messaging core — the same
TurnDriver (credential redaction, tool-approval ladder, SEL audit) every other
channel uses.
| File | Role |
|---|---|
feishu/client.py | lark-oapi WebSocket receive + REST reply |
feishu/transport.py | Authorisation (deny-by-default) and normalisation |
feishu/renderer.py | Buffers the turn, sends one reply |
feishu/transport_dispatch.py | Drives TurnDriver, handles /new /compact |
feishu/gateway.py | maybe_start_feishu() boot entry point |
Related docs
- Channel capabilities: the ten-channel matrix — streaming, buttons, uploads, reply length, approval timeout
- Getting Started: install, first run, connecting a channel
- Configuration: the config file and environment variables