zulipmcp: Instructions file for Codex

AGENTS.md

zulipmcp AGENTS.md is an instructions file for Codex, OpenCode from windborne/zulipmcp. It costs 1,506 tokens per session, scanned B, original, from a forked repository, Apache-2.0.

Repository instructions for ZulipMCP, a project that connects the Zulip team chat service to tools for agents. They describe the code structure, public-code safety rules, typing expectations, and technical implementation details.

In plain words
What is it for?
Use them when changing, reviewing, committing, or integrating the ZulipMCP codebase, especially its API wrappers, MCP tools, event listener, and agent backends.
Why use it?
They help contributors avoid exposing private information or breaking the separation between the Zulip library and its MCP tool layer. They also record important behavior, such as how event listening works.

Instructions file for CodexOpenCode

Written for Codex and OpenCode: the file is AGENTS.md. Also seen: mentions Codex; mentions OpenCode.

This is windborne/zulipmcp's own configuration. It tells Codex and OpenCode how to work on zulipmcp itself, so it is not a mod to install elsewhere. Copy it as a starting point and replace the rules that are about this project. Everything zulipmcp configures →

Reuse

Borrowing it

Nothing to install: this file belongs to windborne/zulipmcp. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.

Copy the file
curl -O https://raw.githubusercontent.com/windborne/zulipmcp/main/AGENTS.md
Clone the repo
git clone --depth 1 https://github.com/windborne/zulipmcp

Made for: Codex, OpenCode.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for zulipmcp AGENTS.md

README.md
[![agentmods](https://agentmods.dev/badge/instructions/windborne/zulipmcp/agents-md/github.svg)](https://agentmods.dev/instructions/windborne/zulipmcp/agents-md)
Your own site
<a href="https://agentmods.dev/instructions/windborne/zulipmcp/agents-md"><img src="https://agentmods.dev/badge/instructions/windborne/zulipmcp/agents-md/github.svg" alt="Measured on agentmods" height="20"></a>

Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.

agentmods 80×15 button for zulipmcp AGENTS.md

Your own site · 80×15
<a href="https://agentmods.dev/instructions/windborne/zulipmcp/agents-md"><img src="https://agentmods.dev/badge/instructions/windborne/zulipmcp/agents-md.svg" alt="Reviewed on agentmods" width="80" height="20"></a>
Per session 1,506 This file is loaded in full into every session.
When invoked 1,506 The same file — it is already loaded in full.
Security scan B 1 finding. A grade says what 26 rules found in the file — not that it is safe.
Origin fork From a forked repository.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5.1 $0.01506 $0.01506
Opus 5.5 $0.00602 $0.00602
Sonnet 5.5 $0.00301 $0.00301
Haiku 4.5 $0.00151 $0.00151

Measured 23d ago against content hash dbbf37211225, method: parsed. Prices are Anthropic first-party input rates as of 2026-10-07, from the pricing page.

Security

Grade B, and why

zulipmcp AGENTS.md scanned grade B with 1 finding against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 23d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Reads MCP configurationmediumAgent snooping

mcp.json carries server URLs and auth tokens; reading it lets a mod discover and abuse other integrations.

- **Codex MCP config is not `.mcp.json` native.** The listener translates `.mcp.json` into Codex `-c mcp_servers...` overrides. Keep secrets in env/header fields; env refs in command/args/cwd/url must fail closed to avoi
AGENTS.md · 40 lines

How it starts

The opening of the file, as written. The whole thing — 40 lines — stays where its author put it; the contents beside it link to each section on GitHub.

zulipmcp

Public open-source repo. All code, commits, and PRs are visible to the world.

Rules

  • Nothing WindBorne-specific. No internal URLs, emails, Zulip instances, or infra references.
  • No internal context in PRs, commits, or comments.
  • Elegant, general code. Type hints everywhere. Minimal dependencies.

Architecture

  • core.py — Zulip API wrappers. No MCP dependency. Returns Python objects.
  • mcp.py — MCP tool layer + SessionState. Thin wrappers over core.py. Hooks system lives here.
  • agent_backends.py — Claude/Codex/OpenCode command builders for the listener. No Zulip API dependency.
  • hermes_plugin/zulip/ — Native Hermes gateway transport. It does not use MCP session state.

Separation is load-bearing: core.py must stay MCP-agnostic so it works as a standalone library import.

Gotchas

  • listen() is async, everything else is sync. It runs get_events() in a thread executor to interleave MCP keepalive pings. Don't make other tools async — FastMCP handles sync tools fine.
  • Event queue narrow does NOT filter reactions. is_dismiss_reaction() receives reactions from all streams — it fetches the reacted-on message to verify stream/topic. Don't remove that check.
  • _session is module-level singleton. One session per process. Don't add multi-session — MCP is one-client-per-server.
  • reply() checks for missed messages before sending. The last_seen_message_id bookkeeping is subtle — trace it carefully before changing.
  • Send tools enforce MAX_MESSAGE_LENGTH. Zulip silently truncates over-limit messages and still reports success — the guard returns an error to the caller instead. It measures the normalized form (post normalize_zulip_markdown()), since normalization can grow content. Override with ZULIP_MAX_MESSAGE_LENGTH for non-default realm caps.
  • Two dismiss-check paths exist intentionally. listen() catches via events, reply() via REST poll. Both needed — user might react during tool execution (not listening).
  • Interrupt-file reads must go through _consume_interrupt_file(). It claims via atomic rename before reading, so a writer replacing the file mid-consume can't have its content deleted unread — and it decodes with errors="replace" so a bad-bytes file can't wedge every future poll. Don't inline a read+delete elsewhere.
  • ZULIPMCP_READ_ONLY is enforced only by core._ReadOnlyAdapter on get_client()'s session (refuses every non-GET). WRITE_TAG hiding is UX; tag every new write tool. A bare requests call or a second zulip.Client bypasses the guard, and a read needing a non-GET would weaken it, so add neither. Unknown flag values must keep raising.
  • Private stream security is asymmetric on purpose. Unset BOT_ALLOWED_PRIVATE_STREAMS = no access (default-deny); only __ALL__ unlocks DM reads. Unset BOT_ALLOWED_WRITE_STREAMS = all writes allowed (backwards-compat). Don't "fix" the asymmetry. Tools don't re-check: new narrow reads must go through fetch_narrow, whose _check_read_scope refuses any narrow that doesn't name an allowed scope before fetching, then drops strays via _is_readable. Single-message reads that return content must go through core.get_message_by_id, whose _is_readable call is their only gate. Never subtract hidden streams with negated terms instead: narrows ride the GET query string and break past ~50 terms.
  • Unread/mention markers read flags off the message dict; missing flags renders no marker. A fetch path that drops them fails silently (message events carry them beside the message; get_events merges them in). unread is human-only (bots never mark read) and skips the account's own messages (API-sent messages start unread for their sender).
  • <msg> blocks are a trust boundary. format_messages builds them: bodies via escape_msg_text (every < → &lt;, Unicode tag characters dropped), attributes via escape_msg_attr (adds ", >); headers printed above blocks (set_context, get_messages) escape names too. Don't exempt code blocks or escape only tag-shaped <msg (anything reading surrounding text is bypassable), and don't escape names in output without blocks (nothing to protect; breaks round-tripping into tool args). Look-alikes are out of scope: this guards who said what, not injection in general.
  • Logging is configured in run_server() only — not at import, not in __main__. Library importers own their root logger and must get no files; wrappers call run_server() directly.
  • configure() must be called before run_server(). run_server() may auto-init a session that reads hook state.
  • auto_typing_start=False gates starts only; listen()/end_session() still send "stop", and reply() sends "stop" in place of its start. Zulip keys the indicator per user, not per process, so gating stops can't isolate an external owner — it would only leave a stale bar on idle/exit, and a reply with no stop leaves the pre-reply status stranded under the new message until the server expiry. Don't gate the stops. set_context() stays a no-op there: a stop that early would wipe whatever the owner posted at boot.
  • Codex MCP config is not .mcp.json native. The listener translates .mcp.json into Codex -c mcp_servers... overrides. Keep secrets in env/header fields; env refs in command/args/cwd/url must fail closed to avoid argv leaks.
  • OpenCode MCP config is not .mcp.json native. The listener translates .mcp.json into inline JSON via OPENCODE_CONFIG_CONTENT. Header values are embedded directly (no env-var indirection like Codex). Env refs in command/args/url are rejected.
  • core.py send paths (send_message/send_direct_message/edit_message) rewrite markdown via normalize_zulip_markdown(). Blank lines are injected before tables and bold/link combos Zulip breaks on are rewritten. Fenced/indented code is exempt from both fixes; inline code spans are exempt only for the bold/link rewrite. The Hermes gateway adapter sends via the raw client and is NOT normalized. ZULIPMCP_MARKDOWN_AUTOFIX=0 disables all of it.
  • remove_reaction() needs explicit reaction_type for custom emoji. Zulip's DELETE reactions endpoint defaults reaction_type to unicode_emoji — custom emoji removals fail silently without it. The listen indicator caches _listen_reaction_type alongside _listen_emoji; always pass both when removing.

Read the full file on GitHub · 40 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 23d ago First seen · 40 lines · 1,506 tokens per session scan B dbbf37211225

Subscribe to this mod's changes

zulipmcp AGENTS.md is an instructions file published in the GitHub repository windborne/zulipmcp (1 stars, last pushed 24d ago), licensed Apache-2.0. It adds 1,506 tokens to every session, about $0.0060 per session on Opus 5.5. A static security scan graded it B with 1 finding (reads mcp configuration). It comes from a forked repository.

Related

zulipmcp AGENTS.md compared with instructions from other repositories

zulipmcp AGENTS.md next to 6 instructions from other repositories
ModTypeStarsLast pushLicenceScanTokensWorks with
zulipmcp AGENTS.mdwindborne/zulipmcpInstructions file124d agoApache-2.0B1,506Codex, OpenCode
vscode buildNext.instructions.mdmicrosoft/vscodeInstructions file195ktodayMITA9,719GitHub Copilot
vscode oss-third-party-notices.instructions.mdmicrosoft/vscodeInstructions file195ktodayMITA5,516GitHub Copilot
next.js AGENTS.mdvercel/next.jsInstructions file143kyesterdayMITA7,461Codex, OpenCode
langchain AGENTS.mdlangchain-ai/langchainInstructions file149ktodayMITA4,680Codex, OpenCode
vscode copilot-instructions.mdmicrosoft/vscodeInstructions file195ktodayMITA3,093GitHub Copilot
vscode model-prompts.instructions.mdmicrosoft/vscodeInstructions file195ktodayMITA2,022GitHub Copilot