Skip to content
Development

Watching Claude work: VS Code in a cmux browser pane

By Victor Da Luz
cmux vscode claude terminal workflow macos

I used cmux for a few days, then stopped. The problem was visibility.

When Claude Code edits files, I want to see the diffs. When it touches five files across three directories in thirty seconds, I need to follow along without losing my place. A terminal log of what changed is not the same as watching it happen in an editor. The mental tax of staying oriented adds up fast, and I was spending more energy tracking what Claude did than reviewing whether it did the right thing.

Visual Studio Code’s Claude integrations did not solve this. Chat mode and the integrated terminal both felt awkward to me. I wanted Claude in its own terminal and the editor in its own surface, both visible at once, not fighting for the same window.

I tried Helix and Neovim. Both are more editor than I need day to day, but neither had the whole VS Code-shaped workflow I wanted. Language servers mean per-project setup. File path clicks from terminal output did not behave the way I expect. The gaps kept pulling me back to VS Code.

What finally worked: run VS Code for the web locally and load it in a cmux browser pane. Claude gets a terminal on the left. VS Code gets the browser on the right. I watch the file tree, open buffers, and git state update as Claude works, without alt-tabbing.

The layout

Four panes:

  • Top left - Claude Code terminal
  • Top right - VS Code for the web in the cmux browser
  • Bottom left - setup terminal (renames the workspace, picks a color, prompts for an optional session name, then stays open)
  • Bottom right - serve-web launcher (mostly a background process; see below)

Every file Claude touches shows up in the tree and in diffs quickly. No window shuffle. Less context loss.

Running VS Code for the web locally

The code serve-web subcommand runs a local copy of VS Code for the Web you can open in a normal browser tab.

code serve-web --port 3740 --without-connection-token --host 127.0.0.1

Use a fixed port. The web client stores a lot of profile state in the browser (per origin: host plus port). If the port changes every launch, you get a new origin and a fresh profile. 3740 is arbitrary; any stable port is fine.

On disk, this mode still uses a server-side tree under ~/.vscode-server/data/User/ on my Mac, separate from ~/Library/Application Support/Code/User/. I symlink the JSON I care about so I am not editing two copies:

mkdir -p "$HOME/.vscode-server/data/User"

ln -sf "$HOME/Library/Application Support/Code/User/settings.json" \
  "$HOME/.vscode-server/data/User/settings.json"

ln -sf "$HOME/Library/Application Support/Code/User/keybindings.json" \
  "$HOME/.vscode-server/data/User/keybindings.json"

Upstream serve-web has a long history of bugs and behavior shifts around which settings are read from disk versus what lives only in the browser profile (example discussion). Treat the symlinks as “keep my files single-sourced where the server still looks,” and expect the fixed port to matter most for theme-like UI state that sticks to the browser.

For light and dark mode to follow the system:

{
  "window.autoDetectColorScheme": true
}

The serve-web launcher pane

The bottom-right pane runs this on launch:

code serve-web --port 3740 --without-connection-token --host 127.0.0.1 2>/dev/null &
until curl -sf "http://127.0.0.1:3740" >/dev/null 2>&1; do sleep 0.3; done
SURF=$(cmux tree --workspace "$CMUX_WORKSPACE_ID" 2>/dev/null | grep '\[browser\]' | grep -o 'surface:[^ ]*' | head -1)
cmux browser --surface "$SURF" goto "http://127.0.0.1:3740/?folder=$PWD"
cmux move-surface --surface "$SURF" --index 0 --focus true
wait 2>/dev/null; true

Step by step:

  1. Start the server in the background. If port 3740 is already taken (two workspaces open), the start can fail quietly; the rest of the script still runs.
  2. Poll until HTTP responds with curl (a simple GET is enough). If a server was already up, this returns immediately.
  3. Find the browser surface in the current workspace. CMUX_WORKSPACE_ID is set by cmux in terminals it spawns.
  4. Point the browser at VS Code for the web with ?folder=$PWD.
  5. Move that surface to the front tab and focus it.

This is not the unrelated code-server project. It is the built-in code serve-web path from the VS Code CLI.

The setup script

The shebang targets Zsh. Workspace initialization lives in ~/.config/cmux/workspace-setup.sh instead of inline JSON. I learned that the hard way: inline commands get echoed as raw text when cmux types them into the terminal. A script file avoids that and keeps ANSI color escapes reliable.

#!/usr/bin/env zsh

clear

printf $'\033[38;5;208mClaude\033[0;37m + \033[38;5;33mVS Code\033[0m\n\n'

_colors=(Red Crimson Orange Amber Olive Green Teal Aqua Blue Navy Indigo Purple Magenta Rose Brown Charcoal)
_color=${_colors[$((RANDOM % 16 + 1))]}
_id=$(printf '%04x' $((RANDOM % 65536)))

cmux rename-workspace "Claude + Code [$_id]"
cmux workspace-action --action set-color --color "$_color"

printf $'\n\033[0;37mSession name (optional, Enter to skip): \033[0m'
read -r _session_name
[[ -n "$_session_name" ]] && cmux rename-workspace "$_session_name"

unset _id _color _session_name _colors

exec $SHELL

Each workspace picks a random color from cmux’s named presets. The four-digit suffix keeps multiple tabs distinct. If I enter a session name, it replaces the generated title.

Full cmux.json

cmux loads workspace commands from ~/.config/cmux/cmux.json (or per-project cmux.json); see Custom Commands for the schema.

{
  "commands": [
    {
      "name": "Claude + Code",
      "description": "Claude + VS Code development workspace",
      "keywords": ["vscode", "claude", "dev", "code"],
      "restart": "confirm",
      "workspace": {
        "layout": {
          "direction": "vertical",
          "split": 0.65,
          "children": [
            {
              "direction": "horizontal",
              "split": 0.45,
              "children": [
                {
                  "pane": {
                    "surfaces": [
                      {
                        "type": "terminal",
                        "name": "Claude",
                        "command": "claude"
                      }
                    ]
                  }
                },
                {
                  "pane": {
                    "surfaces": [
                      {
                        "type": "browser",
                        "name": "VS Code",
                        "url": "about:blank"
                      }
                    ]
                  }
                }
              ]
            },
            {
              "direction": "horizontal",
              "split": 0.55,
              "children": [
                {
                  "pane": {
                    "surfaces": [
                      {
                        "type": "terminal",
                        "name": "Terminal",
                        "command": "~/.config/cmux/workspace-setup.sh",
                        "focus": true
                      }
                    ]
                  }
                },
                {
                  "pane": {
                    "surfaces": [
                      {
                        "type": "terminal",
                        "name": "serve-web",
                        "command": "code serve-web --port 3740 --without-connection-token --host 127.0.0.1 2>/dev/null & until curl -sf \"http://127.0.0.1:3740\" >/dev/null 2>&1; do sleep 0.3; done; SURF=$(cmux tree --workspace \"$CMUX_WORKSPACE_ID\" 2>/dev/null | grep '\\[browser\\]' | grep -o 'surface:[^ ]*' | head -1) && cmux browser --surface \"$SURF\" goto \"http://127.0.0.1:3740/?folder=$PWD\" && cmux move-surface --surface \"$SURF\" --index 0 --focus true; wait 2>/dev/null; true"
                      }
                    ]
                  }
                }
              ]
            }
          ]
        }
      }
    }
  ]
}

The one thing that still breaks

File path clicks in the terminal open in desktop VS Code, not the browser pane. VS Code for the web does not expose a URL scheme for “open this path in my existing tab,” and I am not aware of a supported API for that. There is no clean workaround short of patching the server.

In practice it is less annoying than it sounds. The browser pane already shows edits live. For navigation, Cmd+P in the web pane is fast. What I miss most is jumping straight from a compiler error line in the terminal into the right buffer; that still happens only occasionally.

For plain filename clicks (not OSC 8 hyperlinks), cmux’s app.preferredEditor setting routes opens to desktop VS Code:

{
  "app": {
    "preferredEditor": "code"
  }
}

Not perfect, but it is the best split I have found between watching Claude work and still being able to use the terminal output.

Reflection

I fact-checked this against current code serve-web --help on my machine: --host, --port, and --without-connection-token are still listed. --user-data-dir shows up in older docs and issues but not in this build’s help output, which matches the upstream confusion about what is honored in web mode.

If you try the layout, start with the fixed port and the launcher script, then add symlinks only if you confirm your build reads those paths. The homelab inventory files do not cover any of this; it is entirely local tooling.

If cmux ships UX changes to cmux tree or browser surface IDs, the GNU grep pipeline is the first thing I will revisit.

Related reading

Development

Making my AI agents default to MCP for Plane

My coding agents kept reaching straight for the Plane REST API. I wrote a rule to make MCP the default instead. Here is why, and the handful of cases where direct HTTP is still the right call.

Read

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.