There is a file in my dotfiles called bash.txt: a short list of shell facts I did not want to forget, accumulated over years. The entries have no explanations and no applications attached. One entry reads:
:<<'' empty line terminates the HEREDOCThe trick: a heredoc’s delimiter can be the empty string, in which case the first blank line ends it, and :—the null builtin—discards the body. A block comment for the price of one builtin. When I wrote it down it was a curiosity about a corner of heredoc grammar, useful for exactly nothing I had.
Years later it is the send button.
The protocol
My dotfiles now carry a file of aliases and functions for talking to AI coding agents—Claude, Codex—from any shell, including from inside another agent’s Bash tool. The core of it is a family of comma-suffixed aliases that open a heredoc straight into the CLI:
alias "Claude,= Claude << '$AIEOF'"Alias names can contain commas, so Claude, is a valid name—and a deliberate one, because it makes the invocation read as an address. At a prompt you type Claude,, then your message on the following lines, and then you send it. What “send” means is decided by one line:
AIEOF=$(test "$AILVL" = 0 || echo "${AISID:-}")AILVL is the nesting depth, exported and incremented every time one agent shells out to another. At depth 0—a human at a terminal—AIEOF is the empty string, the alias expands to Claude << '', and the bash.txt trick fires: the first blank line terminates the heredoc. You type your message and press Enter twice. A blank line is the send gesture here, falling directly out of heredoc grammar.
When the call is nested—an agent calling an agent—AIEOF takes the exported session identifier instead. In the Claude path, that is a UUID, so the body can contain blank lines and ordinary output; only the exact UUID on its own line terminates it. Ordinary agent output is vanishingly unlikely to contain that marker by accident. The Codex adapter still returned --last for its session identifier at this revision, so the UUID framing was complete only for Claude.
The UUID is collision-resistant framing, not a security boundary against a hostile process or hostile content with environment access. AISID and the derived AIEOF are exported; a nested process that inspects its environment can reproduce the delimiter exactly. The same heredoc is the convenience at depth 0 and the nested-message framing at depth 1.
Grammatical shell
Roles are aliases too, and their trick is a single trailing space:
alias "Customer= AIMOD=customer "
alias "Engineer= AIMOD=engineer "
alias "Architect= AIMOD=architect "The shell rule being exploited: alias expansion normally applies only to the first word of a command, but when an alias’s value ends in a blank, the next word is checked for expansion too. So
Architect Claude,is simultaneously a grammatical English sentence—a role, a name, a comma—and a valid shell command that expands to AIMOD=architect Claude << '...'. The role rides in as an environment-prefix assignment, which works on functions, not just binaries—that fact is another entry in bash.txt (localenv=somvar some_function). And the Claude function itself wraps the real CLI through command claude, stepping below the alias/function layer to reach the binary—the overload ladder from the same file. Three entries from the trick list, operating in concert.
The functions carry the session state: a claude() wrapper threads AILVL=$((AILVL+1)) plus caller and callee role tags into every invocation, and Claude() picks interactive versus piped mode with test -t 0 -a -t 1. Every nested shell knows its depth, who is calling, and who is being called, entirely through exported environment variables.
The guards
Giving agents a phone system means deciding who may call whom. The dispatch function refuses several shapes of call outright, each with its own message:
- Depth is capped at three levels of nesting: “recursive call depth exceeded”.
- An agent cannot call itself—caller and callee tags are compared before anything runs, and the block message says so plainly: “you (Architect Claude) cannot call yourself”.
- An agent cannot call its own role in disguise, a separate check the source annotates with an honest TODO about role confusion.
- Engineers cannot call anyone; the block message points them back at whoever called them. The bottom of the hierarchy talks through stdout only.
And one guard exists because of observed behavior rather than design foresight: agents receiving a message framed this way sometimes try to respond in kind, by invoking User, as if the human were one more callee. There is a function waiting for them:
User () { ai::sys::block "use stdout instead"; }The fragility is real and accepted: at depth 0 the message body cannot contain a blank line, because the blank line is the send. That is the cost of using an empty delimiter for the interactive form.
The list was load-bearing
The empty-delimiter heredoc sat between a note about declare and a note about aliasing {, equally inert. I could not have predicted which lines would matter. The list just recorded what was true about the shell, one fact per line, and kept being there.