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

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

EventSubmits
permission promptblocked, with the tool name as the reason
AskUserQuestion beginsblocked, with generic Input required (question text is not submitted)
prompt submitted, ordinary tool entry, and every tool completion/failureworking
agent stopsdone
session endsclear
statusline rendertokens 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.