Monorepo for Aesthetic.Computer aesthetic.computer
core slab docs inbox-drain.md
3.9 kB
Markdown
at main

inbox drain — messages into a running agent session, no keystrokes #

slab/bin/inbox-drain.mjs is the hook-side half of the prox inbox. Other agents write to $SLAB_HOME/inbox/<session_id>/messages.jsonl (see prox-inbox.mjs); the drain runs as a lifecycle hook inside the target session and hands pending messages to the model at a turn boundary. The session id is the one the harness passes to its hooks — the same id Slab's ledger records.

Output is one block:

Messages from other sessions (via prox inbox):
[inbox from neo:sip · 2026-09-23 17:58 · reply: prox_send handle="neo:sip"]
  │ look at the diff

Claude Code #

Wired in .claude/settings.json (project layer), two hooks, timeout 5 s:

  • UserPromptSubmit → inbox-drain.mjs prompt. Drains. If anything was pending, prints {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"…"}}. Claude Code injects that as a system reminder alongside the user's prompt; nothing shows in the transcript.
  • Stop → inbox-drain.mjs stop. Peeks; if pending, drains and prints {"decision":"block","reason":"…"}. Claude Code keeps the turn going and shows Claude the reason, so a message that lands mid-turn is read at the end of that turn instead of waiting for the next human prompt.

Delivery is therefore at turn boundaries: the next prompt, or the end of the current turn. A session idle at the prompt gets the message when the human next types — the Stop hook already fired. Empty inbox: the hook prints nothing and exits 0. Any internal error (missing prox-inbox.mjs, unreadable file) goes to stderr and still exits 0; the prompt is never lost to a broken hook.

loop guard #

Stop hooks receive stop_hook_active: true when the turn is already a continuation caused by a Stop hook. The drain lets the turn end whenever the inbox is empty, which covers that case: a continuation only happens because a message was consumed, so the next Stop sees an empty inbox and stays quiet. Fresh messages that arrive during a continuation still go through; Claude Code's own cap ("overrides the hook and ends the turn after 8 consecutive blocks") is the backstop against a peer that never stops talking.

Codex CLI #

Codex hooks (stable, on by default in 0.156) use the same schema as Claude Code: same hooks.json shape, same UserPromptSubmit → hookSpecificOutput.additionalContext, same Stop → {"decision":"block","reason"} with a stop_hook_active input. One difference: Codex's Stop "expects JSON on stdout when it exits 0. Plain text output is invalid", so the Codex wiring passes a codex flag and the drain answers {} when the inbox is empty.

slab/codex/hooks.json carries both hooks with absolute paths (Codex has no $CLAUDE_PROJECT_DIR). Install, one of:

  • user layer: copy or merge it into ~/.codex/hooks.json (merge if that file already exists — Codex loads every source, so a duplicate would double-fire);
  • project layer: <repo>/.codex/hooks.json, which only loads once the project's .codex/ layer is trusted.

Then open Codex and run /hooks: "Before a non-managed hook can run, Codex requires you to review and trust the exact hook definition. Codex records trust against the hook's current hash, so new or changed hooks are marked for review and skipped until trusted." The trust lands in ~/.codex/config.toml under [hooks.state] as a trusted_hash, which is why that file isn't edited from the repo. Changing the command line (even the path) needs a fresh trust. --dangerously-bypass-hook-trust skips the review for one invocation.

Known Codex quirk: additionalContext is currently rendered as a visible developer message in the transcript (openai/codex#16933), so inbox messages show up on screen there rather than silently.

Easel pro #

Easel's pro runtime takes messages over its socket; the hook drain isn't used there. Covered in Easel's own docs.