Watching Claude work: VS Code in a cmux browser pane
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-weblauncher (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:
- Start the server in the background. If port
3740is already taken (two workspaces open), the start can fail quietly; the rest of the script still runs. - Poll until HTTP responds with curl (a simple
GETis enough). If a server was already up, this returns immediately. - Find the browser surface in the current workspace.
CMUX_WORKSPACE_IDis set by cmux in terminals it spawns. - Point the browser at VS Code for the web with
?folder=$PWD. - 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
The trailing blank lines EditorConfig did not fix
Cursor kept padding the end of my files with blank lines. I reached for EditorConfig, learned it does not actually solve that problem, and found the pre-commit hook that does.
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.
Cursor Tips: Optimize Your Workflow
Explore essential tips for using the Cursor IDE to enhance your development workflow, from organizing chats to using AI effectively.
Ready to Transform Your Career?
Let's work together to unlock your potential and achieve your professional goals.