Skip to content
flowerornamentPublic

About

Agent-to-agent communication in tmux

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

hail

Messages between coding agents — Claude Code and Codex — that share one machine and one tmux server.

An agent sends a message to another agent's seat (its workspace). hail writes the message to that seat's inbox and types a one-line envelope into the recipient's prompt. The recipient's hook puts the body in its context at its next prompt or tool call, and that writes a receipt the sender can check or wait on. No daemon, no database, no pane-reading.

[hail kind:ask from:api-1a/%5 reply:api-1a id:1006T171200-a3f1] Review src/auth.ts before the merge; reply with your verdict — hail inbox

Why

Agents working in parallel need to hand each other rulings, reviews, holds and outcomes. Typing whole messages into each other's panes has three problems:

  • Context cost. Codex keeps every user message verbatim through compaction, so a relayed message rides on every later API call. In one pane, relays took 36% of every request.
  • No delivery fact. The sender has to read the other pane to learn whether the text landed or was answered, and every read costs a full-context call.
  • Fragile typing. Long text cannot be verified as typed, and a terminal is not a transport: dialogs, drafts and scrolled panes eat keystrokes.

hail keeps the body out of the prompt, makes delivery a fact you can query, and types only a short triage line.

How it works

  sender (seat api-1a)                             recipient (seat api-1b)
  hail api-1b ask <<'EOF'                            Codex or Claude Code, idle or busy
  <headline>                                      ┌───────────────────────────────────┐
  <body>                 (2) one-line envelope    │ [hail kind:ask from:api-1a …]     │
  EOF            ───────────────────────────────▶ │ (typed, verified, Enter)          │
    │                                             └──────────────┬────────────────────┘
    │ (1) body written first                      (3) prompt hook: hail deliver
    ▼                                                            │ claims the body,
  ~/.local/state/hail/seats/api-1b/new/<id>.md ──────────────────┘ adds it to context
                                                  seats/api-1b/cur/<id>.injected.md
  (4) hail sent <id>  → injected 2026-10-07T09:36:53Z     (the receipt)
      hail await <id> → blocks until there is one
  1. The body is written first. It goes to the recipient's Maildir inbox, durably, before anything is typed.
  2. The envelope is typed. It goes into the recipient agent's pane: hail leaves copy mode, refuses a pane showing a permission dialog, types, checks the text appeared, and presses Enter. If no agent is running there, the message waits in the inbox and the send exits 5.
  3. The hook delivers. On the recipient's next prompt, or after its next tool call when it is busy, its hook (hail deliver) claims the body by renaming the file, so exactly one reader wins, and adds it to the agent's context, framed so a body cannot pass for another message.
  4. The sender checks. The claimed file is the receipt: hail sent and hail await read it, and nobody reads a pane.

Seats

A seat is the workspace an agent works in, named after the directory. Going up from the working directory:

  1. the first .hail-seat file (its content is the name);
  2. else the jj workspace root or git root, by its basename.

Identity comes from the directory only, never from the process tree or TMUX_PANE. Codex runs every command under one shared daemon that carries another pane's TMUX_PANE, and that misattributed messages in 0.3. Run hail from your own workspace; hail whoami shows the seat.

When several agents share one directory, each Claude pane gets a sub-seat, <seat>@<pane> (for example hail@%28). A send to the bare seat is refused with the sub-seats listed. A Codex agent needs its own jj workspace, because its commands cannot be told apart by pane.

Kinds

kind what it does
ask Asks for a reply: any message with --re <id> (an fyi for an outcome). A verdict leads the headline: GO: land it. No state.
fyi Needs no reply; not typed where the recipient's hooks run. No state.
announce Act on it now. No state.
hold In effect, and shown to the seats involved, until anyone sends release --re <id> or it lapses (8h, or --for up to 7d).
release Lifts a hold.

announce, hold and release are control kinds: typed in full, with no body, to act on at once. Why five: docs/2026-10-19-five-kinds.md.

Install

Nix with Home Manager

# flake inputs — the `release` branch always points at the latest tag
hail.url = "github:flowerornament/hail?ref=refs/heads/release";

# home-manager
imports = [ inputs.hail.homeManagerModules.default ];
programs.hail = {
  enable = true;
  skill.enable = true;                                      # link the agent skill
  skill.targets = [ ".agents/skills/hail" ".claude/skills/hail" ];
  # envelopeMax = 400;                                      # HAIL_ENVELOPE_MAX
};

The flake advertises the flowerornament Cachix cache. Builds for aarch64-darwin, aarch64-linux and x86_64-linux are substituted rather than compiled; accept the flake config, or add the substituter yourself. Leave hail's nixpkgs input alone: inputs.nixpkgs.follows changes the build, so the cached one no longer matches and hail compiles from source.

Other ways

  • nix build github:flowerornament/hail?ref=refs/heads/release, or nix run github:flowerornament/hail -- --help.
  • A release tarball from GitHub (aarch64 and x86_64, macOS and Linux). It holds hail and the skill under share/hail/skills/hail.
  • cargo install --path . from a checkout. The pinned toolchain is in rust-toolchain.toml.

Requires tmux, and nothing else: hail talks to no issue tracker.

Hooks

Install them once, at user level. Every project picks them up, and nothing is needed per repository:

hail setup     # shows the diff, asks, writes it (--yes to skip asking, --check to report drift)
hail doctor    # one line per check; every problem names its fix

hail setup edits these files:

  • Claude Code: ~/.claude/settings.json, plus $CLAUDE_CONFIG_DIR/settings.json when that is set to another directory.
  • Codex: ~/.codex/config.toml.

It replaces older hail lines in place, and keeps your other hooks and your comments. The commands it installs:

event command
SessionStart hail brief --hook
UserPromptSubmit hail deliver --format claude (or codex)
PostToolUse hail deliver --format claude --event PostToolUse (or codex): mail reaches a busy agent at its next tool call

Things to know about hooks:

  • Codex trust: Codex runs a changed hook only after you trust it with /hooks, in each session.
  • After an upgrade, run hail setup --check: a release can add a hook (0.7 added PostToolUse for Claude Code), and hail setup replaces older hail lines, including the [ -n "$TMUX_PANE" ] || exit 0; form, in place.
  • New sessions only: both harnesses read hooks when a session starts.
  • Hooks are optional. Without them the envelope still arrives, and the agent runs hail inbox for the body. Hooks never fail a session: errors go to ~/.local/state/hail/hook-errors.log.

Upgrading from 0.3

hail 0.6 no longer imports 0.3 state. Migrate with hail 0.5.x first, then upgrade:

nix run github:flowerornament/hail/v0.5.5 -- migrate

It moves the 0.3 inboxes, receipts, obligations and pending sends into the current layout and parks the old tree in archive/0.3; seats it could not match to a live pane are kept apart as legacy-%N. Without it, 0.6 ignores the 0.3 tree and its mail.

Labels are gone: a seat is named by its directory.

Use

hail whoami                                   # this directory's seat
hail seats                                    # every seat: agent panes, unread, last hook read, root

# Send. The first line is the headline (the ask and the why); the rest is the body.
hail api-1b ask <<'EOF'
Review src/auth.ts before the merge; reply with your verdict
The refresh path is auth/refresh.rs:40-120. Coverage: /tmp/cov.txt
EOF
#   id=1006T171200-a3f1

hail api-1b fyi 'gate green'               # headline only, no body
hail sent 1006T171200-a3f1                    # delivered | injected <t> | read <t> | inline <t> | unknown
hail await 1006T171200-a3f1 --timeout 900     # block until a receipt (--any for the first of several)

# Answer, lift.
hail api-1a fyi --re 1006T171200-a3f1 '87% coverage; refresh path uncovered'
hail api-1b release --re 1006T165000-1c2e 'gate green'

hail brief                                    # what you were sent, holds, late sends
hail inbox                                    # unread bodies (the hook usually delivers them)
hail show 1006T171200-a3f1                    # one message by id, from any seat

How to send:

  • Quote the heredoc delimiter (<<'EOF') so nothing in the body is expanded: backticks, $() and quotes arrive byte for byte.
  • Headline length: a headline over 400 characters is folded into the body. A control kind over the cap is refused instead.
  • --scope <s> says what a message applies to; --no-submit types without pressing Enter.
  • --force skips the dialog guard. Use it only after hail read <seat> 10.

Exit codes:

code meaning
0 typed and submitted, or a quiet fyi
1 usage or state error; the message says what to run
2 control-kind headline over the cap
3 seat problem: no seat here, a shared seat addressed bare, or a name bound elsewhere
4 the target shows a permission dialog
5 in the inbox but not submitted (no agent running, typing not confirmed, or Codex is holding it). Do not resend: it arrives on their next prompt, unless hail says no hook reads that seat or its workspace is gone

read, type and keys drive panes that are not agents: a shell, a gate run, a prompt waiting for y. Read before typing; the guard enforces it.

hail help is the map, and hail help <topic> has the details: send, kinds, receive, seats, panes, setup, state.

For agents

skills/hail/SKILL.md is the instruction set agents load. With the Home Manager module's skill.enable, it is linked where Claude Code and Codex discover skills.

Troubleshooting

Start with hail doctor. It checks:

  • the seat for this directory and its root;
  • tmux, the agent panes, and this seat's wake pane or sub-seats;
  • the hooks in each harness config;
  • unread mail waiting in seats where no agent sits, and seats whose workspace was deleted;
  • whether the state is due for hail gc.
symptom cause and fix
A send exits 5 Written to the inbox, not submitted: no agent runs in that seat, the text did not show in 10 s, or Codex is holding it (the message says where). It arrives on the recipient's next prompt (or its next session's brief). Do not resend.
sent stays delivered The recipient's hook has not run since. Either it has not had a prompt yet, or its hooks are not installed or trusted (hail doctor; in Codex, /hooks).
A send exits 3 "has N agents" Several agents share that directory. Address one sub-seat, as listed, or give each agent its own jj workspace.
A send exits 3 "no seat here" You are outside a workspace. cd to it, or add a .hail-seat file naming it.
A send exits 4 The target shows a permission dialog. Read it (hail read <seat> 10), resolve it, and resend.
Nothing arrives in a session The hooks are read at session start, so start a new session after hail setup. Check ~/.local/state/hail/hook-errors.log.
doctor notes unread mail in a seat no hook reads Mail sent to a name no agent uses any more. Read it with hail show <id>; once that seat's workspace is deleted, hail gc archives it.

State and environment

Everything lives under $XDG_STATE_HOME/hail (default ~/.local/state/hail), outside every repository:

path holds
seats/<seat>/new/<id>.md unread messages
seats/<seat>/cur/<id>.<how>.md claimed (injected, read or inline); the mtime is the receipt time
seats/<seat>/pending/<id> its sends with no receipt yet
holds/<id> holds in effect; delete one to lift it silently
ids/<id> the id index (a symlink to the seat)
archive/ hail gc output and the 0.3 tree

Read mail is kept until hail gc archives it (by default, mail older than 90 days); doctor says when gc is due. gc also archives the mailboxes of seats whose workspace was deleted, once no hook has read them for as long. Each prompt gets at most five bodies, about 8 KB, and the rest arrive on later prompts. The brief shows five entries per section (--all for everything) and a send without a receipt after two minutes. It drops a send that has lapsed after seven days.

variable default what it does
HAIL_ENVELOPE_MAX 400 headline cap in characters
HAIL_SOCKET $TMUX, else the default server tmux socket
HAIL_SEAT — a seat name, used only where the directory names none
HAIL_AGENT_COMMANDS claude,codex commands that count as agents
XDG_STATE_HOME ~/.local/state the state root

Hooks and receipt checks touch only the filesystem: about 2.5 ms of CPU each, with no subprocess. A send's own work takes under 20 ms, plus a 300 ms pause before Enter so the agent's composer does not treat the text as a paste.

Compatibility

hail is the one binary and send the one send verb: there are no aliases. A send is hail <seat> <kind> [headline] with the body on stdin; --kind and --body are refused with a usage error that shows that form. 0.6 removed the 0.3 spellings, and the CHANGELOG names what replaces each.

Develop

just check     # fmt, clippy, unit and integration tests, release-script tests, tmux scenarios, CPU bench
just build     # the Nix package
just land      # gate the described jj change, then move main to it and push

The gate:

  • Lints: clippy runs pedantic, with unwrap and expect denied.
  • Scenarios: test/run.sh runs its scenarios on a scratch tmux server and a scratch state root, so it never touches yours. It needs bash 4 or later.
  • On a loaded machine, set HAIL_TEST_DELIVER_MS=100.
  • CI runs the gate on Linux and macOS with the toolchain pinned in rust-toolchain.toml.

Work happens in jj, with one workspace per agent, and just land is the only way to publish. AGENTS.md has that workflow and the rules for working on hail; DESIGN.md has the principles; the spec, design notes and studies are in docs/; the hook blocks for a hand install are in hooks/README.md.

Release

The version lives in one place, Cargo.toml.

# in a jj workspace
just release-bump X.Y.Z   # set Cargo.toml and Cargo.lock; scaffold the CHANGELOG entry
$EDITOR CHANGELOG.md      # move Unreleased under the new heading; replace the TODO bullet
jj describe -m "X.Y.Z: <summary>" && just land
                          # wait for the Nix Cache workflow on that commit to succeed
# in the colocated checkout
jj git fetch && jj new main
just release-verify       # versions agree, changelog filled, clean tree, the gate, Nix build
just release-tag X.Y.Z    # asks to confirm; checks every system's build is in Cachix, tags, moves origin/release

Every push to main builds the package for each system, pushes it to Cachix and proves it substitutes. The tag publishes a GitHub release with that CHANGELOG section as its notes and binaries attached. Consumers that track the release branch pick it up with nix flake update hail (or nx upgrade hail).

About

Agent-to-agent communication in tmux

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages