workmux

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT

README

workmux

Run several coding agents at once — one git worktree each — from a browser.

workmux

It serves the repo you're standing in at http://127.0.0.1:4315. A single static binary, no runtime, no config, and it works from a phone.

Status: the Go rewrite is in progress. The dashboard, the API and the the agent↔worktree mapping, terminal sessions, changes, actions and the MCP panel are the MCP panel are being ported from the original (a ~4,500-line Python implementation that has been in daily use); see Roadmap. The main branch always builds and passes its tests.

Why

Changes come in different sizes. Some are a backend tweak that needs nothing running; some need Docker up to test at all. You start three of them, hand each to an agent, and then spend your day answering the question which of these wants me right now — across terminal tabs, worktrees, and containers that may or may not be up.

workmux makes a piece of work the unit: a worktree + branch, the agents living in it, the PRs it has produced, its diff, and — only if the change needs it — its own container stack. The list is ordered by what wants you: needs-input first, then working, then whatever has containers up, then the rest.

Two details it gets right, because both were bugs first:

  • "Working" means a live process, not what an agent last wrote about itself. Agent state is written at turn boundaries, so the session you are watching work reads idle.
  • The base checkout sorts last however busy it looks. It's where you happen to stand, not a change in flight — and worktrees live inside it, so a naive prefix check marks it permanently busy.

Install

brew install trivial-corp/tap/workmux            # macOS
npx workmux                                      # no install at all
curl -fsSL https://raw.githubusercontent.com/trivial-corp/workmux/main/scripts/install.sh | sh
go install github.com/trivial-corp/workmux/cmd/workmux@latest
docker run ...                                   # see Docker below
git clone … && make build                        # from source

npx workmux works because the npm package contains the binary — one small package per platform, each declaring its own os/cpu, so npm downloads exactly one and skips the rest. No postinstall script, nothing fetched at install time, and no second runtime: node's only job is to exec the binary it already has. (An earlier attempt shipped a node shim around a python server, which needed two runtimes to run one file. This is the opposite of that.)

All of them give you the same single static binary. Requires git; docker only if your project has a stack and gh only for PR titles — both optional, and their absence costs you that column and nothing else. macOS and Linux.

Homebrew gets a cask rather than a formula, because what ships is a pre-built binary — which makes it macOS-only; on Linux use the install script, the container image, or go install.

While this repo is private, brew and the install script can't reach the release assets. go install works with repo access (GOPRIVATE=github.com/trivial-corp), and building from source always works.

Docker

For a box without a Go toolchain — a homelab server, or managed alongside other services. See deploy/compose.yaml; the short version:

docker run -d -p 4315:4315 \
  -v /srv/myapp:/srv/myapp -w /srv/myapp \
  -v "$HOME/.claude:$HOME/.claude" -e HOME="$HOME" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e WORKMUX_TOKEN=pick-something \
  ghcr.io/trivial-corp/workmux --root /srv/myapp

The repeated paths are the point, not boilerplate. Agent state records absolute directories and a stack started through the mounted socket runs on the host daemon, so everything must sit at the same path inside the container as outside. A repo mounted at /repo produces a dashboard that agrees with nothing.

Two things to know: mounting the docker socket is root-equivalent access to the host, so only do it on a machine you'd already trust this tool on; and the image ships no coding agent — bake yours in (RUN npm i -g @anthropic-ai/claude-code) or run with "agent": null.

On a homelab box, from your laptop

Cross-compile here, install there. No Go toolchain, no checkout, no CI on the far side — and the architecture comes from the box itself, because guessing it is the one mistake that quietly produces a binary that won't run:

make deploy HOST=homelab                        # → ~/.local/bin/workmux
make deploy HOST=root@nas DEST=/usr/local/bin

Then on the box:

cd /srv/myrepo && workmux init && workmux --host 0.0.0.0

To keep it running, deploy/workmux.service is a user service — workmux runs your worktrees, your agent and your docker, so it should be you; as root it would hand a shell to anyone who reaches the port.

cp deploy/workmux.service ~/.config/systemd/user/     # then edit the path + token
systemctl --user enable --now workmux
loginctl enable-linger $USER                          # survive logout

Off loopback a token is required. It still speaks plain HTTP, so put Tailscale, a reverse proxy with TLS, or an SSH tunnel in front — ssh -L 4315:127.0.0.1:4315 homelab needs no token at all and is the safest way in.

Several at once

Running one per project is the normal case, so the browser tab and the header both lead with the project: trip1 · workmux, nas · workmux. Give each one its own port:

workmux --root ~/code/trip1                 # 4315
workmux --root ~/code/other --port 4316

Setting it up

workmux init

It reports what it found — name, base branch, worktree location, stack, agent, and the gitignored files a new worktree would be missing — and writes config only for that last item, because it's the one thing no amount of runtime detection can work out. Everything else is derived from the repo, so most projects get:

  Nothing to configure. Every default fits this repo — just run workmux.

At a terminal it asks about the few things it can't derive — whether to carry those files over, where the app opens, whether this project uses an agent at all — and shows you the file before writing it. Prompts, not a full-screen TUI: it's a handful of questions once, the answer is usually "nothing to configure", and the real interface is a browser.

workmux init --dry-run    # look without writing
workmux init --yes        # take every default, ask nothing
workmux init --force      # replace an existing workmux.json

Piped, scripted or run by an agent it asks nothing and takes the defaults, so it's safe in automation. Agents have their own instructions in AGENTS.md.

Configuration

None required. Drop a workmux.json at the repo root when a default is wrong:

{
  "name": "myapp",                        // default: the repo directory's name
  "worktrees": {
    "path": ".claude/worktrees",          // default
    "copy": [".env", "config/*.key"]      // gitignored files each new worktree needs
  },
  "agent": {                              // defaults are Claude Code; null for none
    "command": "claude",                  // what a new agent session runs
    "spawn": "claude --bg {prompt}",      // how New work starts one
    "attach": "claude attach {id}",       // how to take over a running one
    "jobs": "~/.claude/jobs",             // where its state.json files live
    "mcp": "claude mcp"                   // enables the MCP panel
  },
  "stack": {                              // omit to detect; null for "no app"
    "compose": "compose.yaml",            // default: the compose file it finds
    "slots": "myapp{n}",                  // default: "{name}{n}" — one per work
    "url": "http://{slot}.localhost",     // default: none, so no Open button
    "profiles": "app,tools",              // default: $COMPOSE_PROFILES
    "commands": {                         // default: plain `docker compose`
      "up": "docker compose -p {slot} -f {compose} up -d --build",
      "restart": "...", "stop": "...", "logs": "..."
    }
  }
}

Anything left blank is a capability the UI doesn't offer, rather than a button that fails:

you leave out you get
a compose file, or "stack": null worktrees, agents, sessions and diffs — no app controls at all
agent.spawn New work makes the worktree without starting an agent
agent.mcp no MCP panel
"agent": null no agents anywhere; worktrees and shells

{slot}, {compose}, {profiles}, {path} and {base} are substituted into stack commands, so a project with its own wrapper script (per-slot databases, a reverse proxy to attach) keeps using it.

Options

workmux [options]

  -p, --port N       port to listen on (default 4315)
      --host ADDR    interface to bind (default 127.0.0.1)
      --token TOK    pin the token instead of minting one
      --root DIR     repository to serve (default: the current directory)
      --no-terminal  dashboard only — don't serve shells
      --open         open a browser once it's listening

Every flag has a WORKMUX_* environment variable (WORKMUX_PORT, WORKMUX_HOST, WORKMUX_TOKEN, WORKMUX_ROOT, WORKMUX_TERMINAL=0).

Reaching it from your phone

workmux --host 0.0.0.0

It prints a LAN URL with a token. Off-loopback requests need it; loopback stays exempt, so nothing changes on the machine itself. ?t=… is swapped for an HttpOnly cookie on first load.

This hands out a shell over plain HTTP. That's why it binds loopback only by default, refuses cross-origin WebSocket upgrades (CORS doesn't cover WebSockets), and requires a token off-box. Put TLS in front before exposing it beyond a network you trust, and use --no-terminal if you only want the dashboard.

API

The browser is one client; a mobile app is another. Everything the UI renders comes from the same JSON:

endpoint
GET /api/work the whole dashboard: worktrees, agents, PRs, stacks, sessions, capabilities
GET /api/config the resolved project shape
GET /api/health liveness, outside the token check so a proxy can probe it
GET /api/session/list live sessions
POST /api/session/new {kind, cwd, cols, rows} — shell / agent / attach / logs / git, or resume for "this worktree's agent, whatever that is"
POST /api/session/kill {id}
WS /api/session/socket/{id} binary frames are raw PTY bytes both ways; text frames are JSON control ({"t":"size"}, {"t":"exit"})
POST /api/upload a pasted image; puts it on the host clipboard and returns the path
GET /api/changes ?path=&base= — file status with line counts, plus the commits this branch has that its base doesn't
GET /api/diff ?path=&file= for a file, ?path=&rev= for a commit
POST /api/new {prompt, name?, base?} — a worktree, its files, and an agent on the task
POST /api/stack {action, slot, path} — up / restart / stop, through the project's own commands
POST /api/update {path, base} — merge the base branch in, aborting on conflict
POST /api/pr {ref} — check a PR out into its own worktree
GET /api/mcp, POST /api/mcp/add, /api/mcp/remove the agent's server registry, with reachability
GET /api/log this process's own log

Two rules the session API keeps, both server-side, because between them they're the difference between a dev tool and a remote shell for anyone who can reach the port: a session may only open in one of this repository's worktrees, and a WebSocket upgrade must come from an origin this instance is actually reachable at (CORS does not cover WebSockets).

Roadmap

  • Config resolution with defaults, optional stack, configurable agent
  • Worktrees, agents, PRs, stacks, ordering — GET /api/work
  • Token auth, origin allowlist, embedded UI
  • Terminal sessions: PTY + WebSocket, replay on reattach, size arbitration
  • Changes: status, per-file diffs, commits this branch has that its base doesn't
  • Actions: new work, start/stop a stack, merge the base in, check a PR out
  • MCP panel: reachability, scope, authenticate
  • The frontend as a proper TS build, embedded — shared with a mobile app
  • Release binaries, Homebrew, and an npm package that ships the binary
  • Paired device tokens and TLS, for the remote/mobile case

Using it on another project

Point it anywhere; nothing has to be installed in the project itself:

workmux --root ~/code/my-drupal-site

With no workmux.json it reads the repo: the name from the directory, a stack from whatever compose file it finds, and Claude Code as the agent. A stack the project already has running is recognised whether it's named after the directory (what docker compose up does) or numbered per worktree.

Two things are worth setting for a project that isn't structured around per-worktree stacks:

{
  "stack": { "url": "http://localhost:8080" },   // so there's an Open button
  "agent": null                                   // if you don't use one here
}

Developing workmux

Run it from source, against any project. No build, no install, no CI:

make dev ROOT=~/code/some-project            # add PORT=4321 to sit beside another instance

Against a monorepo with a stack and 40-odd worktrees, that looks like:

$ make dev ROOT=~/Projects/trivial-corp/trip1 PORT=4321
  workmux dev — trip1
  http://127.0.0.1:4321
  dev: frontend from …/internal/web/dist — edit and refresh
  root: /Users/tomas/Projects/trivial-corp/trip1

16:32:19  2.506s gh pr list --state all --limit 400 --json number,headRefName,…
16:32:19    79ms docker compose ls --all --format json
16:32:19    87ms docker compose -p trip1 -f …/compose.yaml ps --all --format json
16:32:19    16ms pgrep -f claude

— which tells you immediately that gh pr list is 2.5s of the first poll (cached for 20s after), rather than leaving you to guess.

That's go run, so a code change is a Ctrl-C and a re-run — about a second. Two things make it a debug loop rather than a guessing game:

  • --dev serves the frontend from internal/web/dist instead of the copy embedded in the binary, with caching off. Editing the UI is a refresh.

  • Every subprocess is logged with its duration and exit code. Every fact this tool reports comes from a git, docker, gh or agent-CLI call, so a wrong dashboard is nearly always a surprising command result, and this shows you which one:

    16:24:01    9ms git -C /repo symbolic-ref --quiet --short refs/remotes/origin/HEAD  exit 1:
    16:24:01   16ms git -C /repo worktree list --porcelain
    16:24:02  310ms docker compose ls --all --format json
    

    --verbose alone gives you that against the embedded frontend.

make test-race                # the concurrent read path, under the detector
make watch ROOT=~/code/app    # restart on every .go change (watchexec or entr)
make debug ROOT=~/code/app    # under delve, with breakpoints
make install                  # ~/go/bin/workmux — what `bin/dev serve` finds
make test                     # go test ./...
make lint                     # gofmt check + go vet

Nothing here needs a push, a tag or a CI run. CI exists to catch what a laptop misses (Linux, and the four cross-compiled binaries), not to stand between you and running the code.

Tests use real git repositories in temp directories rather than mocks: every claim this tool makes about worktrees, branches and commit counts comes from git's own output, so a fake would only test the fake.

CI

ci.yml on every push and PR: gofmt + go vet, tests, tests under the race detector, and a build — on both macOS and Linux, because this shells out to platform tools (pgrep, lsof) and that's where the difference bites. Then the four cross-compiled binaries, goreleaser check, and a Docker build that has to start and answer for itself.

release.yml on a v* tag: four static binaries with checksums, a Homebrew cask, and a multi-arch image to ghcr.

License

MIT

Directories

Path Synopsis
cmd
workmux command
Command workmux serves one repository's dashboard.
Command workmux serves one repository's dashboard.
internal
actions
Package actions is everything that changes something: new work, a stack up or down, merging a base branch in, checking a PR out.
Package actions is everything that changes something: new work, a stack up or down, merging a base branch in, checking a PR out.
agents
Package agents answers two questions the dashboard is built around: which agents live in which worktree, and which of them are working right now.
Package agents answers two questions the dashboard is built around: which agents live in which worktree, and which of them are working right now.
changes
Package changes answers "what has this piece of work actually done".
Package changes answers "what has this piece of work actually done".
config
Package config resolves everything project-shaped from workmux.json — and, when it isn't there, from what the repository already says.
Package config resolves everything project-shaped from workmux.json — and, when it isn't there, from what the repository already says.
gitx
Package gitx reads the repository.
Package gitx reads the repository.
initcmd
Package initcmd is the bootstrap: it looks at a repository, says what workmux will do with it, and writes config only for the things it cannot infer.
Package initcmd is the bootstrap: it looks at a repository, says what workmux will do with it, and writes config only for the things it cannot infer.
mcp
Package mcp surfaces the agent's MCP registry.
Package mcp surfaces the agent's MCP registry.
presets
Package presets turns "I want a session of this kind here" into something runnable, using only what config says.
Package presets turns "I want a session of this kind here" into something runnable, using only what config says.
prs
Package prs reads pull requests through the gh CLI.
Package prs reads pull requests through the gh CLI.
run
Package run is the one place this tool shells out.
Package run is the one place this tool shells out.
stack
Package stack reads the project's containers — one stack per piece of work.
Package stack reads the project's containers — one stack per piece of work.
term
Package term owns the terminal sessions.
Package term owns the terminal sessions.
testrepo
Package testrepo builds throwaway git repositories for tests.
Package testrepo builds throwaway git repositories for tests.
web
Package web is the HTTP surface: the API the browser and (later) the mobile app both read, plus the embedded frontend.
Package web is the HTTP surface: the API the browser and (later) the mobile app both read, plus the embedded frontend.
work
Package work assembles the one thing this tool is about.
Package work assembles the one thing this tool is about.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL