# Hermes Feishu Gateway

> Operate and troubleshoot Hermes via Feishu (Lark): group authorization, pairing/allowlists, Gateway restarts, markdown rendering, and lark-cli setup.

- Skill: `tyrantlucifer/hermes-feishu-gateway` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tyrantlucifer/hermes-feishu-gateway`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tyrantlucifer/hermes-feishu-gateway/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: TyrantLucifer (https://skillmd.com/u/tyrantlucifer)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tyrantlucifer/hermes-feishu-gateway

---


# Hermes Feishu Gateway

How to operate Hermes effectively through the Feishu (Lark) messaging platform. Covers message formatting behavior, lark-cli integration, and known pitfalls.

## Message Formatting (Critical)

### How Feishu Outbound Messages Are Rendered

The Feishu adapter (`plugins/platforms/feishu/adapter.py`) has a three-way dispatch in `_build_outbound_payload`:

1. **Markdown table detected** (`_MARKDOWN_TABLE_RE`) → **ENTIRE message sent as plain `text`** (no markdown at all)
2. **Markdown hints detected** (`_MARKDOWN_HINT_RE`) → sent as `post` type with `md` tag elements (renders headings, bold, italic, code blocks, links, blockquotes)
3. **Neither** → sent as plain `text`

**⚠️ PITFALL: A single markdown table causes the ENTIRE message to degrade to plain text.** All bold, headings, code blocks, links — everything loses formatting. This is because Feishu's post-type `md` elements cannot render tables.

### What Renders in Post Mode (`md` tag)

| Feature | Renders? |
|---------|----------|
| `# Headings` | ✅ |
| `**bold**` | ✅ |
| `*italic*` | ✅ |
| `` `inline code` `` | ✅ |
| ` ```code blocks``` ` | ✅ (isolated as separate fenced segments) |
| `[links](url)` | ✅ |
| `> blockquotes` | ✅ |
| `---` horizontal rules | ✅ |
| `~~strikethrough~~` | ✅ |
| `<u>underline</u>` | ✅ |
| `\| tables \|` | ❌ (triggers plain-text fallback for the whole message) |

### Workarounds for Tables

When you need to present tabular data in Feishu:
- **Use aligned plain text** instead of markdown tables
- **Use bullet lists** with key-value pairs
- **Split the message**: send the table as a separate plain-text message, and the rest as markdown
- **Use Feishu interactive cards** (msg_type=interactive) for structured layouts — the adapter supports this for exec approval prompts

### Configuration

- `display.final_response_markdown` controls CLI display only (`render` | `strip` | `raw`), NOT gateway output
- Changing it to `keep` preserves markdown in CLI; does not affect Feishu rendering
- The Feishu adapter decides format purely based on content pattern matching

## Feishu CLI (lark-cli) Setup

### Installation

```bash
# Install globally (use user prefix if no root access)
npm install -g @larksuite/cli --prefix ~/.local

# Install the CLI skills
npx -y skills add https://open.feishu.cn --skill -y
```

### App Binding

Inside Hermes, `lark-cli config init --new` is blocked (prevents parallel app creation). Use:

```bash
# Bind to existing Hermes Feishu app
lark-cli config bind --source hermes --identity bot-only

# Or force a separate app (for more permissions)
lark-cli config init --new --force-init
```

**`--force-init`** gives a browser link for app selection. The CLI waits for completion — set adequate timeout (120s+).

### User Identity Login

Bot identity can't access personal resources (calendar, mail, drive). Login as user:

```bash
lark-cli auth login --domain calendar    # calendar access
lark-cli auth login --domain im          # messaging access
lark-cli auth login                      # all domains
```

This also gives a browser link for OAuth authorization.

### Permission Errors

When you get `app_scope_not_applied` errors, the error message includes:
- `console_url` — direct link to the Feishu developer console with the required scopes pre-filled
- `missing_scopes` — exact scope names needed

Share the `console_url` with the user so they can add scopes in the developer console.

## Task & Calendar Queries

```bash
# Today's agenda (default)
lark-cli calendar +agenda --format pretty

# Custom date range
lark-cli calendar +agenda --start 2026-06-22 --end 2026-06-28 --format pretty

# My tasks
lark-cli task +get-my-tasks --format pretty
```

**Note:** Tasks may accumulate massively over time (200+). When summarizing, filter by date range and prioritize by deadline.

## Feishu Document Creation via lark-cli

Create Feishu docs from local markdown files using `lark-cli docs +create`:

```bash
# Write content to local file first (avoids shell escaping issues)
# Then create doc from file
lark-cli docs +create --doc-format markdown --title "文档标题" --content @path/to/content.md
```

Update existing docs with `lark-cli docs +update`:

```bash
lark-cli docs +update --doc D0BBdowH7oJwU5xcGM2lwwcogzh --doc-format markdown --command overwrite --content @content.md
```

**Key flags:**
- `--doc` (not `--token`) — document URL or token
- `--command overwrite` — replaces entire document content
- `--command append` — appends to end
- `--content @file.md` — read from local file (relative path only, not absolute)
- `--doc-format markdown` — use markdown format

**Pitfall:** `@file` only accepts relative paths from cwd. Write files in the working directory, not `/tmp/`. Absolute paths like `@/tmp/file.md` fail with `unsafe file path`.

## Session Architecture

- Session key format: `agent:main:feishu:dm:{chat_id}`
- Same Feishu DM window = same session (persists across days until `/new` or `/reset`)
- Session ID contains date timestamp but is NOT a routing mechanism
- Different platforms (feishu/weixin/cli) have independent sessions
- Gateway caches AIAgent instances per session_key for prompt caching

## Group Authorization Troubleshooting

When the bot responds to the owner but silently ignores newly added group members, check authorization before investigating Feishu event subscriptions or mention parsing.

- `Unauthorized user: ou_xxx ... on feishu` proves the event reached Hermes and was rejected by Gateway authorization.
- `FEISHU_GROUP_POLICY=open` alone is insufficient: Gateway still requires pairing, `FEISHU_ALLOWED_USERS`, or explicit `FEISHU_ALLOW_ALL_USERS=true`.
- Unauthorized group messages are silently dropped; pairing prompts are offered only in DMs.
- Prefer per-user DM pairing or a fixed allowlist. Enabling `FEISHU_ALLOW_ALL_USERS=true` is platform-wide and can also expose DMs, not just the current group.
- A config-file change is not active until the running Gateway reloads or restarts. Never report it as effective before that boundary.

For the complete diagnostic flow, safe repair options, restart constraints, and verification checklist, read [references/feishu-group-authorization.md](references/feishu-group-authorization.md).

## Cron Delivery from Feishu Topic Threads

A cron job created inside a Feishu topic may run successfully and create its document, yet fail delivery with:

```text
[99992402] field validation failed
```

**Diagnosis:** inspect `~/.hermes/cron/output/<job_id>/` first. If the output contains the final document URL, generation succeeded and only message delivery failed. Then inspect `~/.hermes/cron/jobs.json`: an `origin` containing `thread_id: omt_...` is the important signal.

**Root cause in the affected adapter path:** cron knows the Feishu `thread_id` but has no `reply_to_message_id`. The adapter's create fallback uses the topic ID as `receive_id` with `receive_id_type=thread_id`; Feishu rejects that request with code `99992402`. A real in-topic reply requires an `om_...` message anchor and `reply_in_thread=true`.

**Verified workaround:** deliver scheduled results to the bare group chat instead of `origin` or an explicit topic target:

```text
feishu:oc_<chat_id>
```

This posts to the group's main conversation. Verify both layers:

```bash
hermes send --json --to 'feishu:oc_<chat_id>' 'delivery test'
```

Then run a tiny one-shot `no_agent` cron job to the same bare target and confirm the log contains:

```text
delivered to feishu:oc_<chat_id>
```

Do not claim the cron path is fixed based only on `hermes send`; direct send and cron delivery are separate verification boundaries. The old `last_delivery_error` remains in job state until the next successful full run, even after the target is updated.

## Cron-generated document permissions

A Feishu user who creates a Hermes cron job does **not** automatically become the `lark-cli` document creator. `lark-cli` user OAuth is machine/profile scoped, so all jobs may create documents under one configured user. Posting that private link to a group does not grant access.

For document-producing group jobs, close permissions after every `docs +create`: dynamically list current human group members, grant each `open_id` the requested Drive permission in batches, validate the complete success ledger, and only then deliver the link. A small cron `script` can inject this policy into existing long prompts; cron script paths must be relative to `~/.hermes/scripts/`.

Do not use bot document fetch as proof that `openchat` sharing works for human members: application identity may not inherit chat collaborator access. See [references/feishu-cron-document-sharing.md](references/feishu-cron-document-sharing.md) for the full workflow, commands, verification criteria, and scaling options.

## Upgrade regression: Feishu WebSocket fails with `extra_ua_tags`

Symptom after a Hermes upgrade:

```text
[Feishu] Failed to connect: Client.__init__() got an unexpected keyword argument 'extra_ua_tags'
```

Cause: current Hermes constructs `lark_oapi.ws.Client(..., extra_ua_tags=["channel"])`, which requires the lazy dependency pin declared in `tools/lazy_deps.py` (`lark-oapi==1.6.8` as of Hermes 0.19.0). The code can update while an older SDK such as 1.5.3 remains installed if `hermes update` logs `platform.feishu failed to refresh: pip not available and ensurepip failed`.

Diagnosis:

```bash
hermes gateway status
$HERMES_HOME/hermes-agent/venv/bin/python - <<'PY'
import inspect
from importlib.metadata import version
from lark_oapi.ws import Client
print(version("lark-oapi"))
print(inspect.signature(Client.__init__))
PY
```

Repair on Debian-style venvs that have no in-venv `pip`/`ensurepip`, but do have system pip with `--python` support:

```bash
cd "$HERMES_HOME/hermes-agent"
/usr/bin/pip --python venv/bin/python install --upgrade 'lark-oapi==1.6.8' 'qrcode==7.4.2'
# Prevent future lazy-backend refreshes from failing on the same venv:
/usr/bin/pip --python venv/bin/python install --upgrade pip
hermes gateway restart
```

Verify all boundaries:

1. `Client.__init__` contains `extra_ua_tags`.
2. Gateway journal contains `[Lark] ... connected to wss://msg-frontier.feishu.cn/...` and no new `extra_ua_tags` exception.
3. `hermes send --json --to 'feishu:oc_<chat_id>' 'channel recovery test'` returns `success: true` and a real `om_...` message ID.

Do not re-enter app credentials for this failure: configuration is already loaded; the SDK/API mismatch prevents WebSocket construction before authentication.

## Common Pitfalls

- `npm install -g` without `--prefix ~/.local` → EACCES on shared servers
- `config init --new` blocked inside Hermes → use `--force-init` for separate app
- Gateway cannot restart itself from a terminal/tool subprocess in its own cgroup. Ask the user to run `hermes gateway restart` from an independent server shell, or have an authorized user send `/restart` in chat; do not try to bypass the self-restart guard.
- `auth login --domain <domain>` needs long timeout → use background + notify_on_complete
- Tasks may accumulate massively (200+) → filter by date when summarizing

