An agent-friendly wrapper for HEY CLI (hey), Basecamp's command line for HEY email, calendars, todos, habits, time tracking and journals.
hey-axi runs hey, asks for its JSON response envelope, and prints it as TOON. It follows the AXI principles: a live home view, minimal default fields, truncated long text, definitive empty states, and unknown flags rejected up front. On our synthetic benchmark that's ~89% fewer tokens than hey --json (docs/benchmarks.md). It also adds a few safety rails. It covers every command in HEY CLI v1.8.0 (released 2026-10-09; commit 732568e): 156 runnable command paths, including the login/logout aliases, plus the box <id>-style shortcuts. It routes from a snapshot of HEY's own hey commands --json catalog. Commands added since v1.7.0 (contact deliver, event delete --occurrence/--apply-to, screener clear, thread update) need HEY v1.8.0 or newer; with an older HEY they fail with kind: hey_outdated. See CHANGELOG.md for what changed in each release.
Requirements: Node 20+ and an authenticated HEY CLI (curl -fsSL https://hey.com/install-cli | bash, then hey auth login).
npm i -g hey-axi # puts `hey-axi` on your PATH
# or run it without installing:
npx -y hey-axi box view imbox
# agent skill (see "Agent integrations" below)
npx skills add TangentialSolutions/hey-axiFrom source: git clone https://github.com/TangentialSolutions/hey-axi && cd hey-axi && npm ci && npm link.
hey-axi uses $HEY_BIN if it's set, and otherwise finds hey on PATH. launchd/cron jobs usually have a minimal PATH, so set HEY_BIN=$HOME/.local/bin/hey there.
hey-axi # home view: scope, last session, Imbox count + 10 threads, next commands
hey-axi box view imbox # TOON: summary, a few columns per thread, notice, help
hey-axi box imbox # HEY's shortcut forms work too
hey-axi --account 2 search --from a@b.com # global flags anywhere
hey-axi thread read 123 # bodies cut at 1000 chars with "(truncated, N chars total)"
hey-axi thread read 123 --full # HEY's complete, untouched result
hey-axi box view imbox --fields id,subject,creator.email_address # choose columns (--fields all = every field)
hey-axi thread read 123 --json # the same shaped output as compact JSON
hey-axi thread update 123 --name "Kitchen renovation quotes" # rename a thread (nothing is emailed)
hey-axi box view imbox --quiet # drop HEY's summary/notice/breadcrumbs; keep data, count and hints
hey-axi reply 123 --message - < note.txt # message body from stdin (saved as a draft)
hey-axi setup scope --label Acme # per-directory home view (.hey-axi.json)
hey-axi --version # bare hey-axi version (fast path); `hey-axi version` adds HEY's
hey-axi box view imbox --ids-only # raw HEY output (also --count/--markdown/--html/--styled/--jq)
hey-axi set-aside group view 4 --limit 5 # three-word commands
hey-axi move --help # usage, flags, examples, notes (no HEY call)- Home view.
hey-axiwith no arguments printsbin,description, thescope, a one-linelast_sessionsummary (when session capture is on), acountof threads, the 10 newest threads, andhelplines with next commands (including how to see the rest). If HEY is missing, signed out or failing, it printsstatuswith the fix and still exits 0. - Directory scope.
hey-axi setup scope --label Acme(or--box,--search,--account,--limit) writes.hey-axi.jsonin the current directory. The home view in that directory (or any subdirectory) then shows that label, box or search instead of the Imbox, and carries--accountinto every suggested command.--statusshows it,--removedeletes it, and re-running with the same values is a no-op. - Minimal fields. List commands show at most 4 columns (usually an id, the thread id, sender and subject). The defaults per command are in
src/shape.jsand in each command's--help.--fields a,b,cpicks columns, using default aliases (from,subject,at) or dotted paths (creator.email_address,messages.0.summary);--fields allkeeps everything. An unknown field exits 2 and lists what's available. - Truncation. Strings longer than 1000 characters (120 in list cells) are cut and marked
… (truncated, N chars total), and ahelpline suggests--full. - Aggregates and empty states. Every list gets a
count:N of T totalwhen HEY reports a total (inmetaor next to the list),N totalwhen HEY says the list is complete,N shown; more availablewith ahelpline naming--all/--page/--limitwhen HEY says there's more, andN shown; no more pages reportedwhen HEY says neither. Lists of plain values and results holding several lists (2 todos, 1 habits) are counted too. An empty list printsempty: 0 results for `…`, and a command with no data printsresult: no data returned. Non-JSON success output is wrapped asok: true+output. - Help lines. A list always ends with a next step: HEY's own, or (when HEY gives none) the matching
view/showcommand. HEY's breadcrumbs (next-command hints) becomehelp: Run `hey-axi …` to …, HEY's hints are rewritten to namehey-axi, and--account/--base-urlare carried into them. - Fail loud. Unknown commands (with "did you mean"), unknown flags, flags with a missing or wrongly typed value (
--limit abc), missing or extra arguments (thread readwithout an id), and missing required flags (movewithout--to) all exit 2 before HEY runs, with the valid usage. - Idempotent mutations. For state-setting commands (
seen,todo complete,label add,move,screener approve,timetrack stop, name-basedcreates, …), when HEY's failure says that command's own end state already holds ("already seen", "already completed", "already exists", …), and for deletes whose target is already gone, hey-axi printsok: true,noop: trueand exits 0. The patterns are per command (END_STATESinsrc/policy.js); a generic conflict, any read, any send or reply, amovewhose failure names a different box than--to, "already exists" on a create, and a delete whose not-found names something other than the target stay errors. - Complete per-command help.
hey-axi <command> --helplists arguments, every flag with its type and default, global flags, and 2-3 examples, with no HEY call. --fullturns all of this off, and--jsonprints the same shaped result as compact JSON.
| Kind | Commands | Behavior |
|---|---|---|
| JSON (default) | almost everything | hey … --json → TOON (or compact JSON with --json) |
| Raw output | --ids-only, --count, --markdown, --html, --styled, --jq; shell-completion generate; timetrack export without --output |
HEY's output is printed as-is once HEY succeeds; on failure only a structured error is printed |
| Stream | watch |
Each event printed as it arrives, as a TOON block followed by a blank line (--json: HEY's NDJSON, one line per event); Ctrl-C/SIGTERM forwarded to HEY |
| Needs a person | tui, mcp, the setup wizard, auth login/login without --token/--cookie |
Refused (exit 2) unless --interactive is passed, terminal or not, with the alternative (e.g. pass --token, or ask the user to run hey-axi auth login --interactive in their terminal). With --interactive, HEY gets the terminal (for mcp, its stdio, so an MCP client can register hey-axi mcp --interactive); the others also need a real terminal |
| Captured | other setup … commands, upgrade, auth login --token … |
Run with stdin closed, HEY_NONINTERACTIVE=1 and EDITOR/VISUAL=false, so nothing can wait for input; output is shaped like any other command |
- Nothing is sent without an opt-in (
--allow-sendorHEY_AXI_ALLOW_SEND=1):composeandreplyare saved as drafts. hey-axi adds--draft, and the output starts withsent: false,saved_as: draftand anaxi_notice(also printed to stderr).forward,draft sendandbulk-reply sendhave no draft mode in HEY, so they're refused (exit 2) with the safe alternative (reply … --to,draft show,bulk-reply preview).- If you pass
--draftor--dry-runyourself, hey-axi adds and marks nothing. - With
--allow-send, a send HEY confirms is markedsent: true(and names the delivered message'sid/topic_idwhen HEY reports them, as HEY main does). If HEY holds it for Undo Send (delayed: true), aheldline says it hasn't gone out yet and the thread won't show it until it does. - If HEY refuses to send and keeps the message as a draft (HEY v1.8.0's
not_delivered, usually the sending limit), the result is an error withkind: not_delivered,sent: falseand the draft id, and says not to repeat the send. HEY v1.7.0 reports such a send as sent, so it can't be detected there. - Recipients HEY would silently drop (no domain or top-level domain, like
boborbob@example) are refused (exit 2,sent: false) before HEY runs, forcompose,reply,forwardanddraft edit. HEY v1.8.0's own "not a valid email address" refusal is reported the same way.
- Credentials stay out of agent context.
auth tokenneeds--allow-secretorHEY_AXI_ALLOW_SECRETS=1. - Destructive commands need an opt-in.
screener clear(HEY v1.8.0) moves everything waiting in the Screener to Trash, for every sender, and HEY asks for no confirmation. hey-axi refuses it (exit 2, HEY isn't run, nothing changes) unless you pass--allow-destructiveor setHEY_AXI_ALLOW_DESTRUCTIVE=1. The refusal says what it would do and suggestsscreener listand per-senderscreener denyfirst. - Content-first.
compose,reply,draft edit,journal write,contact note setandbulk-reply sendmust be given their content (--message,--message -for stdin, or a positional) and are refused (exit 2) otherwise, so HEY never opens an editor. - hey-axi's own flags (
--allow-send,--allow-secret,--allow-destructive,--interactive,--fields,--full) are never forwarded to HEY. Switches take no value:--draft=falseor--allow-send=falseis refused (exit 2) rather than guessed at.
Errors are printed as structured TOON on stdout: ok: false, error, a kind, and, when HEY gave them, code, hint and meta, plus a help next step. HEY's error text is translated too: only its first meaningful line is kept, stack traces, panics and terminal escapes are dropped, and debug keys (stack, trace, …) are removed from meta. On success HEY's stderr notices (warnings, next_page: …) go to stderr without that noise.
| Exit | Meaning |
|---|---|
| 0 | success (including no-op mutations and empty results) |
| 1 | the command failed; kind says why: not_found, auth, forbidden, rate_limited, network, api_error, ambiguous, hey_missing, hey_outdated (the installed HEY is older than the command needs), not_delivered (HEY kept a send as a draft), command_error |
| 2 | usage error (kind: usage): unknown command/flag/field, bad flag value, missing or extra arguments, missing required flag or content, a switch given a value (--draft=false), blocked send, a destructive command without --allow-destructive, a person-only command without --interactive, or a usage error reported by HEY |
There are two ways to give an agent hey-axi. You only need one:
- Session hooks (recommended where supported):
hey-axi setup hooksregisters the home view as session-start context, and a session-end hook (hey-axi hook session-end), for Claude Code (~/.claude/settings.json), Codex (~/.codex/hooks.json, and it turns on[features].hooksin~/.codex/config.toml) and OpenCode (a managed plugin in~/.config/opencode/plugins/). Every new session then starts with your Imbox summary and the next commands, and no tool call is needed.--projectwrites to the current directory's.claude/,.codex/and.opencode/instead.--statusreports what's installed, and--removeremoves only hey-axi's entries.- Re-running is a no-op, or repairs the path if hey-axi moved. It uses axi-sdk-js's installer.
- The session-end hook records a one-line summary (at most 300 characters) of what the session did (drafts saved, messages sent, other changes; command names and numeric ids only, never message content) under
$XDG_STATE_HOME/hey-axi(or$HEY_AXI_STATE_DIR), and the next home view shows it aslast_session. Nothing is recorded unlesssetup hooksturned it on, andsetup hooks --removeturns it off. Entries are tagged with the agent session (CLAUDE_CODE_SESSION_IDin Claude Code,CODEX_THREAD_IDin Codex, orHEY_AXI_SESSION_ID) so concurrent sessions in one directory stay apart. OpenCode has no exact session-end event, so its plugin records onsession.idle/session.deleted. - Note that the hook puts Imbox senders and subjects into every session's context.
- Agent skill (broader support, loads on demand): see below.
skills/hey-axi/SKILL.md is an Agent Skills package. It tells coding agents (Claude Code, Codex, Cursor, Gemini CLI, Copilot, …) when and how to use hey-axi: setup checks, triage commands, which id goes where, drafts-by-default, output modes, watch and exit codes. references/commands.md lists every command and is generated from the manifest (npm run skill:gen).
Installing the skill doesn't install the CLI. Install hey and hey-axi first (see Install).
# with the skills CLI (https://skills.sh), from GitHub
npx skills add TangentialSolutions/hey-axi # this project
npx skills add TangentialSolutions/hey-axi -g -a claude-code # user-wide, Claude Code only
# with GitHub CLI (gh skill, preview)
gh skill install TangentialSolutions/hey-axi hey-axi --agent claude-code --scope user
# manually
cp -r skills/hey-axi ~/.claude/skills/ # or ~/.agents/skills/, ~/.codex/skills/, …See docs/listing.md for how hey-axi gets listed on skills.sh and axi.md.
src/manifest.json is a snapshot of hey commands --json plus usage parsed from hey <command> --help. The current snapshot was built from basecamp/hey-cli main at the v1.8.0 release commit (732568e, 2026-10-09), so its hey_version reads 1.8.0. To regenerate it:
npm run refresh-manifest # from whichever `hey` you have installed (runs only `hey version`, `hey commands`, `hey <command> --help`)
npm run manifest:main # clone basecamp/hey-cli main, build it (needs Go and git), and rewrite the manifest from it
npm run check-drift # same build, but only compare: exits 1 with a readable diff when commands or flags differ
node scripts/check-drift.js --ref v1.8.0 --write # a tagged release instead of mainhey-axi routes only from the snapshot, so an unknown command is rejected immediately (exit 2, with suggestions) without running HEY. New HEY commands become available after a refresh and a hey-axi release. The upstream drift GitHub Action builds HEY from basecamp/hey-cli main daily (and on changes to the manifest or scripts) and fails when its commands or flags differ from the snapshot; help-text-only changes are listed in the job summary without failing. It also warns when HEY publishes a release newer than the one the snapshot builds on.
npm test # node:test; every test uses a fake `hey` (test/helpers.js), never the real CLIToken benchmark (HEY CLI vs hey-axi output): docs/benchmarks.md, with npm run bench:tokens.
See AGENTS.md for the code map and conventions, and SCHEDULED_HEY_CLI.md for running triage on a schedule.
MIT © 2026 TangentialSolutions. hey-axi is an independent project. It isn't affiliated with or endorsed by Basecamp or HEY.