agentleFS
basecampbasecamp/hey-cli/docs/agents.md

hey-cli

AGENTS.md423 starsChanged 8 months agoMITView on GitHub

hey-cli is built to be driven by an agent as readily as by a person: every command that returns data answers --json, the exit codes are stable (hey help exit-codes), and hey commands --json describes the whole surface. This page covers the two integrations that ship with it: an agent skill for Claude Code and Codex, and an MCP server. hey-cli ships with an embedded agent skill so your coding agent can work with HEY on your behalf, and a…

  • Reads credentials

The file

Files
agents.md

Coding agents and MCP

hey-cli is built to be driven by an agent as readily as by a person: every command that returns data answers --json, the exit codes are stable (hey help exit-codes), and hey commands --json describes the whole surface. This page covers the two integrations that ship with it: an agent skill for Claude Code and Codex, and an MCP server.

Agent skill and Claude Code plugin

hey-cli ships with an embedded agent skill so your coding agent can work with HEY on your behalf, and a Claude Code plugin (hey@37signals from the basecamp/claude-plugins marketplace). The setup wizard connects every detected agent automatically; these commands manage the integrations on their own:

hey setup claude    # install the skill and the hey@37signals plugin for Claude Code
hey setup codex     # install the shared skill for Codex
hey skill install   # install the skill only (~/.agents/skills/hey, linked for detected agents)
hey setup agents             # non-interactive: skill + a single detected agent (the installer uses this)
hey setup agents --remove    # remove HEY's managed skills and Claude Code plugin
hey doctor                   # check skill and plugin health per detected agent

hey setup agents never prompts and never guesses: with several agents detected it installs the skill only and lists the hey setup <agent> choices. HEY_SETUP_AGENT=claude|codex|all|none picks explicitly. HEY_NONINTERACTIVE=1 disables interactive sign-in for harnesses that run hey under a pseudo-terminal. The installed skill is refreshed automatically the first time a new hey release runs.

Codex discovers the shared ~/.agents/skills/hey skill directly. hey-cli does not also copy it to ~/.codex/skills/hey, which would make Codex list the same skill twice. Setup and version refresh remove that legacy path only when its ownership marker proves an older hey-cli created it; user-authored files are left untouched.

hey only ever writes skill directories it owns: each one it creates carries a .managed-by-hey-cli marker, and install, replacement and automatic refresh all refuse a hey skill directory (or symlink) without it — a hand-authored skill at one of those paths is never overwritten or claimed. hey doctor flags an unmanaged baseline and how to adopt it.

Keychain access from agents

hey keeps your login in the system keyring, which on a Mac is the Keychain. Two things can keep an agent's commands from reading it, and signing in again fixes neither:

  • Codex's sandbox hides the keyring from the commands it sandboxes, so a saved login reads as missing there; hey's hint says so when CODEX_SANDBOX is set. Let Codex run hey outside its sandbox: approve its escalation prompt, or allow read-only hey commands with prefix_rules in a file under ~/.codex/rules/.
  • A process outside your Mac's desktop session, such as one started over SSH, is refused by the Keychain: hey fails with exit status 36 (errSecInteractionNotAllowed) and a hint that a new login won't help. Codex runs its commands and MCP servers from a background server that keeps running after you quit Codex. If that server was started from such a session, every hey command and hey mcp fails this way, sandboxed or not, and a rules file can't help. Quit Codex, run codex app-server daemon restart in Terminal on the Mac, and start Codex again; codex --no-daemon runs without the background server.

hey mcp reads the login before it answers the MCP handshake, so a client that can't use it reports only that the connection closed. The reason is on the server's stderr, which Codex keeps in its log.

MCP server

hey mcp runs an MCP (Model Context Protocol) server on stdin/stdout, serving HEY boxes, search, threads, contacts, todos, calendars, and your identity as tools. Sign it in once, then register it with any MCP client as a stdio server:

hey auth login --mcp                   # hey mcp's own sign-in (read, write and send)
claude mcp add hey -- hey mcp          # Claude Code
hey mcp --read-only                    # serve only read-only actions
hey mcp --domains boxes,search         # narrow the served tool surface

Its own sign-in

hey mcp doesn't use the CLI's credential. hey auth login --mcp signs it in as its own app, "HEY MCP (local)" in HEY's Connected Apps, asking to read, write and send but nothing more: a model driving hey mcp can't reach your settings, forwarding, trash or HEY World, which the CLI's own sign-in can. The credential is kept apart from the CLI's, in its own keychain item (or its own entry in credentials.json when there is no keychain), and it refreshes as that app. HEY_TOKEN never stands in for it. Without it, hey mcp exits and says to run hey auth login --mcp; it doesn't fall back to the CLI's credential, even read-only, because that one holds full access and --read-only is hey mcp's own rule, not HEY's.

The sign-in uses a device code where HEY offers one (it prints a link and a code to check), and the browser otherwise. If you uncheck sending on HEY's page, hey mcp saves drafts but serves no way to send them; sign in again to allow it. Without write it serves only reads. hey auth status shows both sign-ins, and hey auth logout --mcp revokes hey mcp's at HEY and removes it, leaving the CLI signed in.

The app's client id is the same in every HEY. HEY_MCP_CLIENT_ID, or mcp_client_ids in ~/.config/hey-cli/config.json keyed by HEY origin ({"mcp_client_ids": {"https://app.hey.com": "..."}}), overrides it. A repository's .hey/config.json can't set it.

Each domain is one gateway tool (hey_boxes, hey_search, hey_threads, hey_contacts, hey_todos, hey_calendar, hey_identity) dispatching actions derived from the HEY SDK's API model; call an action named describe for any action's parameter schema. Listings with more pages come back as {"next_page": cursor, "results": ...} — pass the cursor back as the action's page parameter. The posting-changes feed's last page comes back as {"next_since": ..., "next_v": ..., "results": ...} — the cursor for the next incremental poll, passed back as the action's since and v parameters. Mutations are never retried automatically: a 429/503 on a write surfaces to the caller rather than risking a duplicate delivery, so retry a failed write yourself once you know it did not land. Logs go to stderr — stdout carries the MCP wire protocol.

Drafts first, sends confirmed

create_message, create_reply and update_message, the three actions that can send on HEY's API, always save a draft: the entry's status is forced to drafted, and the entry takes only its recipients (entry.addressed). A call that asks to send (any other entry.status), schedule (entry.scheduled_delivery and its date and hour) or triage the thread in the same request (mark_done, pop, scheduled_bubble_up) is refused before anything reaches HEY. Updating a draft that was scheduled clears its schedule rather than letting it go out later. A save comes back as {"drafted": true, "draft_id": ..., "location": ..., "app_url": ..., "note": ...}. location is HEY's answer, the draft's resource, which in a browser opens its thread; app_url is the page that opens the draft (/messages/<id>/edit on the Location's origin), left out when the Location is relative or names anything but a draft. HEY acknowledges a draft with 204 and a Location. Any other answer is reported as an error that says the mail may have been sent, so check HEY before trying again. A save that fails with no answer from HEY (a dropped connection, a timeout, a 5xx) may still have been saved, and its error says to check Drafts before retrying.

Delivery is one action in a tool of its own: hey_send's send_draft, with the draft's id. Each call asks you to confirm the email in your MCP client (an MCP elicitation), showing the sending address, To, Cc and Bcc (a dash when empty), the subject, attachments, images and the body's first 1,500 characters, with links' targets beside their text and every control and invisible character escaped so a display name can't forge a line. Whatever it can't show (the rest of a long body, what attachments hold, images, hidden parts or formatting the text leaves out) is named on a Not shown here line beside the draft's link in HEY. What you approve is what goes: the draft is fetched again after you answer, and if its recipients, sender, subject, body or attachments changed it isn't sent. Each draft is sent through a hey mcp process once; another attempt in the next 24 hours, even after a failure, is refused, and the send is never retried, so an unclear outcome says to check Sent.

So when you ask your agent to send a draft, it calls send_draft straight away: the confirmation is where you check what goes out, and saying yes in the chat doesn't send anything. You can still have it show or revise the draft first as you like.

hey_send is served only when the sign-in was granted sending, never under --read-only, and is listed only to an MCP client that can show a confirmation. A client that can't is told to send each draft from its page in HEY (the draft save's app_url) or with hey draft send <id>, and a call to send_draft from it answers with that link, as edit_url, and sends nothing. send_draft also refuses a draft that is already scheduled, has no recipients, or has a recipient with no address. Nothing else a draft holds refuses it: how much your agent sends, and to whom, is yours to decide once you've granted sending.

Earlier releases delivered mail when a call omitted entry.status; these actions now save drafts only. describe for the draft actions serves the draft-only schema, and the hey_threads tool description says the same. A person can still send any draft from HEY or with hey draft send <draft_id>.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.