Remote agent status + git/PR in the sidebar for cmux ssh-tmux mirrors
Draft β remote agent status + git/PR for cmux ssh-tmux mirrors. Builds on the merged remote-tmux work; opt-in behind the existing Remote tmux beta.
What you get
When you attach a remote session with cmux ssh-tmux <host> and run Claude Code or Codex in a mirrored pane, the left sidebar now shows the agent's live status, the git branch, and a clickable PR link β the same rows a local workspace shows, for an agent running on the remote host.
In the mirror workspace's sidebar row you see, for an agent + repo that live entirely on the remote host:
my-workspace
π¦ Claude Code idle
β <branch> Β· /path/to/repo
β <owner>/<repo> #<n> open β clickable, opens the PR
Why this was missing β local vs remote
The sidebar's agent/branch/PR rows are driven by three local producers, and all three are structurally blind to a remote agent:
| Producer | How it works locally | Why it fails for an ssh-tmux mirror |
|---|---|---|
Agent status (SharedLiveAgentIndex + tab.statusEntries) | a hook injected into the locally-launched agent reports over the cmux control socket; liveness gated on a local PID | the agent runs on the SSH host β not in the local process table, and the mirror's panes have no local PTY |
Git branch (CmuxSidebarGit poller) | runs git in the workspace's local working dir | guard isRemoteWorkspace == false else { return } β the poller bails; and the repo is on the remote anyway |
PR (CmuxGit poller) | local git remote slug β GitHub API / gh | same: no local repo to resolve a slug from |
So everything assumed local repo + local cmux-instrumented shell. A mirror has neither β which is exactly why this needed a new channel.
How it works under the hood
The insight: the mirror already holds a live tmux -CC control stream to the host, and tmux can subscribe a client to a user-option value (refresh-client -B). So the remote agent's own lifecycle hook publishes status into a tmux user option, and cmux receives it as a %subscription-changed line on the stream it's already reading. No remote cmux CLI, no socket, no reverse relay, no auth.
βββββββββββββββββββββ your Mac (cmux) ββββββββββββββββββββββ
β sidebar rows ββ workspace.statusEntries / gitBranch / β
β pullRequest β
β β² β
β RemoteTmuxSessionMirror ββ parse @cmux_agent / @cmux_git β
β β² β
β RemoteTmuxControlConnection (subscribes #{@cmux_agent}, β
β β #{@cmux_git} per pane) β
ββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββ
β %subscription-changed (live tmux -CC stream)
ββββββββββͺβββββββββββ SSH ControlMaster ββββββββββββββββββββ
βΌ
βββββββββββββββββββββ remote host ββββββββββββββββββββββββββ
β agent lifecycle hook (installed by cmux on attach): β
β SessionStartβrunning UserPromptSubmitβworking β
β Stopβidle β
β ββ tmux set -t "$TMUX_PANE" @cmux_agent '{β¦}' β
β ββ ( git rev-parse / git diff / gh pr view β
β β tmux set @cmux_git '{branch,dirty,pr}' ) & β
β claude / codex ββ the real agent β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
On attach (RemoteTmuxController.installRemoteAgentStatusHooks, once per host, fire-and-forget): cmux writes a dependency-free hook into the remote ~/.claude/settings.json and ~/.codex/hooks.json over the open ControlMaster (a python3 merger that preserves your other hooks and replaces only cmux-marked ones).
On each agent event the hook (running in the agent's pane + cwd):
- writes
@cmux_agent{agent,state,model?}synchronously (cheap), and - runs
git/ghin a detached background subshell (so latency never delays the agent) and writes@cmux_git{branch,dirty,pr?}.
Both are scoped to $TMUX_PANE, self-disable outside tmux, and print {} for valid blocking-hook stdout.
cmux side: per-pane subscriptions #{@cmux_agent} / #{@cmux_git} (next to the existing cmux_cwd_/cmux_reflow_ ones) β parsed by RemoteTmuxAgentStatus / RemoteTmuxGitStatus β written to the real sidebar models (statusEntries, per-panel gitBranch/pullRequest). No renderer changes.
How this compares to the existing local path
| Local agent | Remote mirror (this PR) | |
|---|---|---|
| transport | cmux control socket (unix) | tmux @-option over the existing -CC stream |
| who reports | hook β cmux hooks claude β¦ | hook β tmux set @cmux_agent β¦ |
| install | PATH shim / wrapper at launch | cmux writes remote settings.json/hooks.json on attach |
| liveness | local PID scan | the hook's Stop event + chip-clear on teardown |
| deps on remote | β | just tmux (+ git/gh for the PR row) |
It deliberately reuses the same sidebar models as local, so the rows render identically β it only swaps how the data crosses the boundary.
Fallbacks (when there's no hook)
For an agent that was already running before attach (its hooks aren't loaded), or a host where the hook can't be installed, cmux degrades gracefully to two coarser signals it can read without the hook:
- Foreground command (
pane_current_commandβ "Claude Code running"), and - Transcript poll β reads the remote
~/.claudetranscript mtime+model over the master for busy/idle (Claude only).
The hook path (Option C) takes precedence when present.
Proof (validated live against a real AL2023 host, tmux 3.6a)
tmux set @cmux_agent/@cmux_gitpushes%subscription-changed cmux_agent_<pane>/cmux_git_<pane>on the control stream β- cmux's generated install command merges cleanly into the remote
settings.jsonβ - the installed hook, run exactly as Claude invokes it (event JSON on stdin) from a repo cwd, set:
@cmux_agentβ{"agent":"claude","state":"working","model":"claude-opus-4-8[1m]"}@cmux_gitβ{"branch":"<branch>","dirty":0,"pr":{"number":<n>,"state":"MERGED","url":"<pr-url>"}}
- both parse to the sidebar models (model
[1m]suffix stripped; PR branch-matched) β - and the screenshot above is the rendered result.
Tests & docs
- Pure parsers/builders fully unit-tested:
RemoteTmuxAgentStatusTests,RemoteTmuxGitStatusTests,RemoteTmuxAgentHookInstallerTests,RemoteTmuxAgentProbeTests, subscription-command tests. All green; test files wired into the pbxproj. - DEBUG checkpoints (
remote.agent.hookinstall/.sub/.hook/.sidebar,remote.git.sub/.git) for live diagnosis. docs/investigations/remote-agent-status-sidebar.md(design + ranked options + empirical findings) andβ¦-test-plan.md(manual test plan AβE + a no-app probe).
Notes / limits
- Hooks load at agent startup β only agents started after attach get the hook path; earlier ones use the fallback chip.
- The git/PR row needs
ghinstalled + authed on the remote and a GitHub remote; refreshes on hook events, not on an idlegit checkout. - All user-facing strings localized (en + ja).
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.
Summary by cubic
Shows live remote agent status and git/PR info in the left sidebar for cmux ssh-tmux mirrors using tmux option subscriptions. Opt-in behind the existing Remote tmux beta.
-
New Features
- Live remote agent chip (running/working/idle + model) via
@cmux_agentovertmux -CC. - Remote git branch and clickable PR via
@cmux_git(branch/dirty/PR). - One-time, per-host hook install into
~/.claudeand~/.codexthat publishes@cmux_agent/@cmux_git; scoped to the pane and non-blocking. - Graceful fallbacks without hooks: foreground command detection and Claude transcript probe for busy/idle.
- No UI changes; writes to existing sidebar models. Tests added for parsers and control parsing.
- Live remote agent chip (running/working/idle + model) via
-
Migration
- Enable the Remote tmux beta and connect with
cmux ssh-tmux <host>. - Ensure
tmuxis present on the remote. For PR info, install and authgitandghon the remote. - Start the agent after attach so the hook loads.
- Enable the Remote tmux beta and connect with
Summary by CodeRabbit
- New Features
- Remote tmux sessions now show supported AI agentsβ running, working, or idle status in the sidebar, with model details when available.
- Remote Git branch and pull-request information is displayed alongside session tabs.
- Added English and Japanese translations for agent status labels.