Agent sessions
Shells show up in the Command Center on their own. Agent state — blocked, error, done — does not: it comes from a plugin running inside the agent that reports to the local Sox broker. This page is the install and configuration procedure, per harness.
Reading this as an agent asked to set yourself up? Everything below is executable. Work the Requirements section, then your harness’s section, then verify with the doctor at the end. Do not guess at a harness you cannot identify — ask.
Requirements
jqonPATH.SOX_ATTENTION_BINset to an absolute path to the broker binary that ships in the app bundle:/Applications/Sox.app/Contents/Resources/bin/soxd. Both bundled names implement the same commands; the broker keeps the internalbin/soxdfilename.- The plugin installed wherever the agent process runs: on the Mac for local sessions, and on each remote host for remote ones.
export SOX_ATTENTION_BIN=/Applications/Sox.app/Contents/Resources/bin/soxd
On a remote sox session this export is unnecessary: the attach path sets SOX_ATTENTION_BIN
and SOX_ATTENTION_HOME in the shell’s environment already, pointing at the deployed remote
broker. The plugin install is still yours to run on that host.
Claude Code
The plugin is published through the repository’s marketplace manifest. Add the marketplace, then install:
/plugin marketplace add 0xsend/sox
/plugin install sox@sox
sox@sox is <plugin-name>@<marketplace-name> — both happen to be sox. After the
release-channel switch, this Git marketplace installs the plugin tree from the newest fully
published Sox release. A local checkout is for development only: give it a separate
marketplace name or use --plugin-dir so it does not replace the production sox source.
Claude Code reports the full state set, including blocked the moment a permission prompt
would render, whether or not the window has focus and whether the agent is local or remote.
Codex
After the release-channel switch, Codex reads the repository marketplace from Git:
codex plugin marketplace add 0xsend/sox
codex plugin add sox@sox
The hook map is shared with Claude Code, so Codex gets the same state set. A local checkout
is an explicit development source, separate from the production sox marketplace.
Existing installs after the release-channel switch
For a Claude Code Git marketplace, update and restart the session:
claude plugin marketplace update sox
claude plugin update sox@sox
Claude Code does not auto-update third-party marketplaces by default. A marketplace added
from a local directory needs a one-time replacement at its original scope. First run
claude plugin marketplace list --json and find sox with "source":"directory".
Read the settings file that declares it to find marketplace scope M: user at
~/.claude/settings.json, project at <project>/.claude/settings.json, or local at
<project>/.claude/settings.local.json. Run project/local commands from that project.
Run claude plugin list --json and record every sox@sox install’s scope S,
projectPath, and enabled state. Then:
claude plugin uninstall sox@sox --scope S --keep-data # each recorded install
claude plugin marketplace remove sox --scope M
claude plugin marketplace add 0xsend/sox --scope M
claude plugin install sox@sox --scope S # each recorded install
Re-disable any install that was disabled, restart, and verify marketplace list --json
shows "source":"github", "repo":"0xsend/sox" without a local path and
plugin list --json shows the release version in cache/sox/sox/. Keep the original
scope and project directory for every command.
For a Codex Git marketplace, run codex plugin marketplace upgrade sox,
codex plugin remove sox@sox, codex plugin add sox@sox, then restart. For a Codex
marketplace added from a checkout, check its root with codex plugin marketplace list,
remove sox@sox and the sox marketplace, add 0xsend/sox, add sox@sox, and restart.
Grok
Grok loads the same Claude-compatible plugin, and the lifecycle events it does emit map
straight through: working while it runs, done when it stops, cleared at session end.
Grok has no focus-independent blocked signal. Its event table does not include the
permission event the other harnesses use, so a Grok session sits at working for the whole
approval wait rather than rising to the top of the roster. Until xAI ships an equivalent
event, bridge it through Grok’s notification hooks. Add to ~/.grok/config.toml, using an
absolute path to the installed plugin’s hook:
[[ui.notifications.hooks]]
command = "bash \"/absolute/path/to/plugin/claude-code/bin/sox-attention-hook\" approval_required"
events = ["approval_required"]
only_unfocused = false
timeout_secs = 5
only_unfocused = false matters: a focused approval dialog is still something the roster
should show. SOX_ATTENTION_BIN must be set in the Grok process’s environment, which Sox does
for surfaces it owns. Approving the tool returns the row to working on its own.
When a Grok session has working sox attention but no bridge configured, the plugin says so once at session start rather than reporting a state it cannot observe.
pi
Sox ships as a pi package — a hooks extension over the same reporting path:
pi install git:git@github.com:0xsend/sox.git
pi’s lifecycle events map onto working, done, and clear. pi has neither a permission event
nor a notification bridge, so attention rests in those three; there is no blocked to report.
The package also installs a /sox-doctor prompt carrying the same diagnostic guidance as the
Claude Code command.
Starting an agent in a shell
There is no --agent flag and no agent verb. sox new and sox up take --exec followed by
-- and an argv, and that argv becomes the shell’s child process:
sox new ae-dev --exec -- claude --resume
Everything after -- is argv, passed through unmodified — no shell quoting, no wrapping, no
prompt parsing. Sox does not know or care that the argv is an agent; the Command Center shows
an ordinary shell row, and agent state arrives on it through the plugin above exactly as it
does for an agent you launched by hand.
A shell whose child is the agent ends when the agent ends. Sox clears attention on
SessionEnd, but the PTY exits with the process, so the row goes away with it. To keep the
shell after the agent exits, pass a wrapper as the argv — that is the recipe, and it stays
yours rather than becoming a sox flag:
sox new ae-dev --exec -- bash -lc 'claude --resume; exec "${SHELL:-/bin/bash}" -l'
The shell drops to an interactive login prompt in the same directory, keeping the agent’s
scrollback, and the Command Center row reverts to manual. Substitute any wrapper: ; exec $SHELL -l to inspect and continue, || exec $SHELL -l to keep it only on failure, or a
script of your own for anything else.
To run the agent unattended and get the terminal back, use new --no-attach:
sox new ae-dev --no-attach --exec -- bash -lc './scripts/nightly.sh; exec "$SHELL" -l'
That executes the argv on the remote host with no confirmation and no sandbox, and returns
immediately. It prints the new shell as ae-dev/<backing-id>, the spelling sox wait
and sox send take for a shell on that host. Attach later with the host-qualified
handle from sox ls.
Steering an agent without attaching
sox send queues one bounded payload into a shell that is already running, which is how you
hand a running agent an instruction without taking its terminal.
Check the target’s latest turn first. Input goes to whatever its terminal is showing. If the
agent has a question or permission picker open, --enter can select the focused option while
the text is lost. Leave an open question for the human to answer.
send refuses, delivering nothing, when it can tell. Exit 45 means the agent reported an open
question: relay it to the human. Exit 46 means the target is running claude, codex or
grok and its attention state is missing or unreadable: run /sox:doctor in that shell. To
pass on an answer the human gave, add --answer; the receipt then says what the guard saw. A
question the agent never reported is not caught, so the check above still applies.
For a target with no open prompt:
sox ls | cat # s3 s3.g1477 live claude /work/lane
sox send s3.g1477 --enter -- 'run the migration, then report'
The target names its generation. A handle is lineage-blind: s3 means whichever shell
holds that slot right now, and shells re-materialize under new generations whenever the daemon
restarts. A send is a blind write, so sox send s3 is refused, and the refusal tells you what
s3 backs at that moment. Piped sox ls prints the generation beside the handle for exactly
this reason; on a terminal, ask the shell’s host for it with sox ls --backing-ids <host>.
A bare target is a shell on this machine. A shell on another host is written
ae-dev/s3.g1477, which is how sox ls prints it, and the send runs on that host over SSH.
No verb asks any other host, so an unrelated host that is asleep or offline never delays or
refuses it.
A delivered send reports what it hit on stderr — sent 34 B to s3.g1477 (cwd /work/lane) —
but that receipt means only that the shell accepted the input. It does not show that the agent
read the text as a message. Confirm the text appears in the target’s transcript before saying
the message was delivered.
Token and cost figures
Row sparklines, token counts, and running cost come from the statusline, not from hooks — no hook event carries them. The statusline is a user-owned setting, so no plugin can write it for you.
Claude Code, in settings.json:
{
"statusLine": {
"type": "command",
"command": "/home/you/.claude/plugins/data/sox-sox/bin/sox-statusline"
}
}
Codex, in config.toml:
[tui.custom_status_line]
type = "command"
command = "/home/you/.codex/plugins/data/sox-sox/bin/sox-statusline"
padding = 0
[tui.custom_status_line.env]
SOX_ATTENTION_BIN = "/Applications/Sox.app/Contents/Resources/bin/soxd"
Each harness allows exactly one statusline command. To keep one you already have, make
sox-statusline the configured command and name yours as the delegate — sox submits its
progress figures and then emits your script’s output verbatim:
export SOX_STATUSLINE_DELEGATE=/absolute/path/to/your-statusline
The delegate must be an absolute executable. Without one, sox renders its own
model | context% | $cost line.
SessionStart creates the launcher under the plugin data directory. Start and exit one
session with the migrated plugin, then run sox-doctor from that plugin to see the exact
launcher path for this host. Replace only the Sox script path in the setting that already
owns the statusline (Claude user/project/local statusLine.command, or Codex
[tui.custom_status_line] command). Keep type, padding, refreshInterval, all env
values and shell exports unchanged. If a personal wrapper probes checkout, marketplace
clone, or versioned cache paths, replace its candidate list with the launcher alone while
keeping its delegate logic. Restart and run sox-doctor: ok means the launcher is in
use; stale-soon names a versioned cache path, main a marketplace clone, and checkout
a development path. The launcher follows the plugin version loaded at the next session
start, including an accepted rollback.
Verifying
Hook failures are swallowed on purpose — a broken submit must never stall an agent — so a
half-configured install reports success and shows nothing. The doctor is how you find out. It
resolves and prints every value that has to agree: the broker binary, which broker home the
submit dials and why, whether that socket answers, which app owns it, the surface key, whether
that key has a cached envelope, and whether jq is present. It reads only; it submits
nothing.
"$SOX_ATTENTION_BIN" attention --doctor
Claude Code also exposes it as /sox:doctor, which runs the check and interprets the result
for you; pi ships the same guidance as the /sox-doctor prompt.
For a failure the doctor calls healthy, trace the submits themselves. Point
SOX_ATTENTION_DEBUG at a writable file and every hook invocation appends the resolved
binary, its arguments, and the broker’s exit code. Diagnostics go only to that file, never to
stdout, so statusline and hook output stay clean. Leave it unset in normal use.
export SOX_ATTENTION_DEBUG="$HOME/.cache/sox/attention-hook.log"
What the plugin sends
| Event | Submits |
|---|---|
| permission prompt | blocked, with the tool name as the reason |
AskUserQuestion begins | blocked, with generic Input required (question text is not submitted) |
| prompt submitted, ordinary tool entry, and every tool completion/failure | working |
| agent stops | done |
| session ends | clear |
| statusline render | tokens and cost, without touching the state |
Submissions go over a local socket, never to the terminal, so nothing the plugin does can
corrupt what you see on screen. If the binary, jq, or the socket is unavailable, the hook
exits successfully and writes nothing.
Notification and approval reason text is passed as a process argument to the local broker and
truncated to 256 characters. That is visible to same-user process inspection while the helper
runs. AskUserQuestion is the exception: question, option, and answer text never become a
reason; only the literal Input required is submitted. Sox accepts the remaining reason path on
the assumption that one operator owns the machine; the environment variables above follow the
same assumption, and none of them should point anywhere you do not control.