SSH agents

Every Sox shell has one SSH_AUTH_SOCK, set once when the shell is born and never changed: <home>/agent/<handle>.sock, where <home> is the session’s Sox home (~/.sox on a remote host) and <handle> is the shell’s handle (s0, s1, …). The shell’s own daemon serves that socket for as long as the shell exists, so git push, ssh, and ssh-add -l inside the shell always find something to talk to.

What answers on the other side is decided by who is attached. A Sox shell has no location of its own — only attaches do:

When several clients are attached, the most recent attach wins. When that client detaches, the shell falls back to the most recent of the others. When no attached client brought an agent, the socket answers no identities until one does: a signature in that shell fails with publickey denied rather than hanging, and sox doctor reports the state as no_client.

Sox routes; it never chooses. Whether a signature needs Touch ID, a passphrase, or nothing at all is the agent’s policy, not Sox’s.

Local attach change: Sox only selects an agent that already lists keys. If Apple’s launchd agent is empty at selection time, ssh-add inside the Sox shell talks to the per-shell relay, not to launchd’s agent, and cannot fill it. Load keys into the Mac’s agent outside the Sox shell. In the app, start a new Sox session (or restart the app) to select it: new panes and reconnects in an existing session reuse its cached selection, even after a probe timeout. A fresh CLI sox attach selects again. The same applies to keys loaded later by AddKeysToAgent. On-disk IdentityFile keys still work without an agent upstream.

Checking a shell

sox doctor <host>:<project> --shell s0

The shells line for the handle ends with the agent columns, for example agent=keyed agent_owner=per_handle_export agent_path=~/.sox/agent/s0.sock agent_target_kind=daemon_served agent_upstream=/tmp/ssh-abc123/agent.4242 identities=2. The states:

agent=Meaning
keyedAn agent answers with identities= keys.
no_clientNo attached client brought an agent. Attach from somewhere that has one.
emptyThe attached client’s agent holds no keys (ssh-add -l on that machine is empty).
danglingThe shell’s socket is gone: its daemon is not running. sox doctor says which.
unreachableSomething else sits at the path and does not speak the agent protocol.
unknownThe shell was created by something other than Sox and inherited whatever it had.

--json carries the same columns as rows[].agent on the shells step, for scripts that want to assert on them before doing work in a shell.

Your ssh config and shell rc

Sox needs nothing in ~/.ssh/config beyond what you already use to reach the host. Forwarding is per attach: a client that connects with ForwardAgent yes for that host brings its agent, one without does not. Sox’s own sessions pass -A when the session was started with --forward-agent.

Do not set SSH_AUTH_SOCK in ~/.zshrc, ~/.bashrc, or a direnv hook on a host you reach through Sox: it replaces the per-shell socket with a fixed path and every attach after the first is ignored. If a dotfile has to set it for other logins, guard it:

[ -n "$SOX_HOME" ] || export SSH_AUTH_SOCK="$HOME/.1password/agent.sock"

A guard on SSH_CONNECTION is not enough. Sox-born shells on a Mac are minted in the console session, so SSH_CONNECTION is unset even when SOX_HOME is set. Sox restores the per-handle socket after rc; the SOX_HOME guard keeps a later export from fighting that restore.

Sox exports SOX_HOME into every shell it creates, so the line is a no-op there and unchanged everywhere else.