How I Built a Persistent Remote Workspace for Developers and Coding Agents A developer guide details how to run a persistent remote development workspace on a Linux VM that hosts repositories, application services, tests, and coding-agent CLIs such as Claude Code and Codex, while Chrome and Playwright MCP stay on the Mac and connect over SSH. The setup uses example ports 3000 for the frontend, 4000 for the API, and 8931 for Playwright MCP, and warns against exposing those ports to the internet, recommending inbound SSH be limited to a trusted source IP or private network. The author notes the workflow is a personal setup, not a benchmark or completed team rollout, and that running an agent CLI on the VM does not mean its language model is self-hosted there. Start the frontend, API, database, browser, IDE, and coding agent. Add another worktree for a second task. A large application can make the laptop feel like the limit of the workflow. I have been experimenting with moving development compute to a remote Linux machine. My Mac remains the place where I give instructions, review code, and watch the application. Herdr organizes the remote workspace around Claude Code and Codex. The visible-browser workflow works in my personal setup. This guide explains how to assemble it, including a simpler direct HTTP connection supported by current Claude documentation. The commands are examples to adapt to your repository; this article is not a benchmark or a completed team rollout. The remote VM runs repositories, application services, development data, tests, and agent CLIs. Chrome and Playwright MCP run on the Mac. SSH carries application requests toward the VM and browser-tool requests back toward the Mac. The arrows show requests; responses return over the same connections. Example ports are 3000 for the frontend, 4000 for the API, and 8931 for Playwright MCP. Replace them consistently if your project uses others. You need a Mac with Chrome, SSH, and a supported Node.js LTS release https://nodejs.org/en/about/previous-releases that meets your tool requirements; a Linux development VM with SSH access; and a repository whose development commands you understand. Agent subscriptions or API access remain separate requirements. Running an agent CLI on your VM does not mean its language model is self-hosted there. Model hosting and provider access remain separate decisions. Environment: VM provider console, then Mac terminal. Start with a dedicated development VM and a non-root account. Use a separate SSH key, verify the server’s host-key fingerprint through the provider console, and keep administrative recovery access available while changing SSH or firewall settings. Limit inbound SSH to your trusted source IP or private network. Do not open ports 3000, 4000, or 8931 to the internet for this workflow. Apply the restriction in the provider firewall and review the host firewall too. The exact commands depend on your distribution and provider; Ubuntu's firewall documentation https://ubuntu.com/server/docs/how-to/security/firewalls/ explains one common implementation. Confirm the SSH server permits the required forwarding and applies GatewayPorts no to this connection. A server configured with GatewayPorts yes can force a reverse listener onto all interfaces despite the client's loopback request. Have the administrator check the effective policy before creating the browser bridge. OpenSSH server configuration https://man.openbsd.org/sshd config.5 GatewayPorts Use development credentials and synthetic or approved development data. Keep production databases, deployment credentials, personal browser sessions, and cloud administrator roles outside the workspace. Review repository scripts before running them: an agent can invoke tools with the permissions its account possesses. For ongoing instance safety, I would make these responsibilities explicit: Readiness check: From the Mac, connect using your actual non-root username and VM address: ssh DEV USER@VM HOST On the VM, whoami should show the development account. Finish key-based access before proceeding; avoid placing private keys or passwords in repository files. Environment: remote VM. Install Git and the runtime versions your repository requires. Follow the official Claude Code quickstart https://code.claude.com/docs/en/quickstart and, optionally, Codex CLI setup https://learn.chatgpt.com/docs/codex/cli . Authenticate each CLI under your development account before installing its Herdr integration. Herdr manages the workspace; it does not install these agents for you. Its integrations expect their configuration directories to exist. Herdr installation https://herdr.dev/docs/install/ and integration requirements https://herdr.dev/docs/integrations/ The documented Herdr installer can be downloaded for inspection first: curl -fsSL https://herdr.dev/install.sh -o herdr-install.shless herdr-install.shsh herdr-install.sh Read the downloaded script before running the third command. Reopen the shell if the installer changes your executable path. Confirm the commands are available: git --versionnode --versionclaude --versioncodex --versionherdr --version Skip the Codex checks and integration if you are only using Claude. Run each chosen agent once to complete its normal login flow, then exit back to the shell. Clone your repository into your development account. The following path and URL are placeholders: mkdir -p ~/workcd ~/workgit clone YOUR REPOSITORY URL my-appcd my-app Follow the repository’s setup instructions. Use its pinned runtime, package manager, and lockfile. For an npm project with a committed package-lock.json consistent with package.json, the dependency-install command is: npm ci See npm’s clean-install requirements https://docs.npmjs.com/cli/v11/commands/npm-ci/ . Configure development-only environment variables. Bind the frontend, API, and development database to private interfaces appropriate to the setup. For this single-VM example, use 127.0.0.1 for the application listeners. For a Next.js project whose dev script invokes next dev , this is an example frontend command: npm run dev -- --hostname 127.0.0.1 --port 3000 These flags are specific to Next.js development commands https://nextjs.org/docs/app/api-reference/cli/next next-dev-options . Vite and other tools use different options. For an API that already reads HOST and PORT , an example is: HOST=127.0.0.1 PORT=4000 npm run dev Environment variables do nothing unless the application consumes them. Check the listener rather than assuming the setting worked: ss -ltnpcurl -I http://127.0.0.1:3000 Readiness check: The frontend responds and the intended listeners show loopback addresses, rather than 0.0.0.0 or :: . Test the API using your repository's documented read-only health endpoint. Fix local VM startup failures before adding tunnels. Install integrations for the agents you initialized, then open the project workspace: herdr integration install claudeherdr integration install codexherdr integration statuscd ~/work/my-appherdr Use separate panes for application services, agents, and tests. Run claude and codex in their intended project directories. If services were started outside Herdr in step 3, stop those test instances normally before restarting them inside panes; avoid duplicate port listeners. Readiness check: You can identify each pane’s job, and the frontend and API still respond. Keep the agent’s task and working directory explicit, especially when using multiple branches. Environment: Mac, dedicated Chrome profile. Create a development-only profile and install the official Playwright extension https://chromewebstore.google.com/detail/playwright-extension/mmlmfjhmonkocbjadbfplnigmagldckm . Sign in only to the development accounts needed for the task. Keep Chrome open. In a Mac terminal, start the MCP server: npx -y @playwright/mcp@latest \ --extension \ --host 127.0.0.1 \ --port 8931 The @latest tag moves. After testing, record the tool versions and replace it with the tested package version when creating a repeatable team setup. Use a terminal without PLAYWRIGHT MCP EXTENSION TOKEN configured. The extension requests connection approval by default; that optional token bypasses the dialog. For this walkthrough, approve the connection yourself and select the intended development tab. Extension setup and approval https://github.com/microsoft/playwright/blob/main/packages/extension/README.md Keep the default host check: do not add --allowed-hosts ' '. The explicit host setting keeps this server on loopback. Playwright MCP configuration https://github.com/microsoft/playwright-mcp configuration Readiness check: The process remains running. In another Mac terminal, inspect its listener: lsof -nP -iTCP:8931 -sTCP:LISTEN Expect 127.0.0.1:8931. If several Chrome profiles have the extension, follow its documented profile-selection option rather than relying on whichever profile was used last. Environment: a separate Mac terminal. Leave the Playwright terminal running. Replace the SSH destination below: ssh -N \ -o ExitOnForwardFailure=yes \ -o ServerAliveInterval=30 \ -o ServerAliveCountMax=3 \ -L 127.0.0.1:3000:127.0.0.1:3000 \ -L 127.0.0.1:4000:127.0.0.1:4000 \ -R 127.0.0.1:8931:127.0.0.1:8931 \ DEV USER@VM HOST -L creates Mac-side listeners for remote application services. -R creates a VM-side listener that reaches Playwright MCP on the Mac. The forwarding connection carries traffic over SSH. OpenSSH forwarding https://man.openbsd.org/ssh.1 ExitOnForwardFailure catches listener-setup failures; it does not prove the destination service is healthy. The keepalive options detect an unresponsive connection and can terminate it after roughly 90 seconds here. They do not reconnect it automatically. OpenSSH configuration https://man.openbsd.org/ssh config.5 Readiness check: Open http://127.0.0.1:3000 in the development Chrome profile. It should show the remote application. On the VM, ss -ltnp should also show the reverse listener on 127.0.0.1:8931. Keep this terminal open. Environment: remote VM, development account. Claude Code supports an HTTP MCP connection directly: claude mcp add --transport http --scope user \ playwright-mac http://127.0.0.1:8931/mcp claude mcp get playwright-mac That endpoint is loopback on the VM ; SSH forwards it to the Mac. The older intermediate mcp-remote process is unnecessary for this configuration. The user scope makes this entry available to that user's Claude sessions, so keep the account dedicated to development. Claude Code MCP configuration https://code.claude.com/docs/en/mcp Restart or open Claude Code in the intended Herdr pane. The get command shows the saved configuration and connection status; /mcp lets you inspect or reconnect the server inside the active session. Claude MCP server status https://code.claude.com/docs/en/mcp server-status For the first request, ask: Use playwright-mac to open http://127.0.0.1:3000 http://127.0.0.1:3000 in my development browser tab. Describe the page and report any visible error. Do not submit forms or change account data. Approve the extension connection on the Mac. Readiness check: You see Claude interact with the expected tab. Codex can share the workspace, but configuring and validating its browser tools is a separate step. Environment: remote Herdr workspace and Mac browser. Choose a small issue. Reproduce it, request a scoped change, inspect the diff, run the repository’s relevant tests, and review the application. Watching browser actions supplements test assertions and code review. To detach from Herdr, press Ctrl+b, then q . Reconnect over SSH and run herdr to return. Detaching keeps the server's pane processes running. A server restart ends those original processes; restoration is a different mechanism. Do not use herdr server stop as a detach command. Herdr persistence https://herdr.dev/docs/session-state/ The browser bridge still needs the Mac awake, Chrome and Playwright MCP running, and SSH connected. A remote agent waiting for that bridge can stall or fail when the Mac disconnects. Test this boundary deliberately with a harmless build or test run. For stronger reliability, plan process supervision, resource limits, backups, and recovery. Persistence alone does not guarantee task completion. SSH reports an occupied port: Inspect listeners on the relevant machine; change the port mapping or stop your known stale process. Do not kill an unidentified process. The app fails in Mac Chrome: Test it on VM loopback first, then check the local forwarding connection and browser API URL. A browser request uses the Mac’s network perspective. Claude cannot reach MCP: Confirm the Mac server, SSH reverse listener, /mcp path, and Claude's /mcp status. Server policy may prohibit forwarding. The wrong Chrome profile appears: Check the extension’s profile-selection instructions and select the development profile explicitly. Browser actions stop after sleep: Wake the Mac, restore Chrome/MCP and the tunnel, then check the agent’s actual state before retrying. Measure startup, rebuilds, test duration, memory, latency, recovery effort, and total operating cost against the same local workflow. Size the VM around evidence. Give each developer an identity and a deliberate isolation boundary. Separate Linux users still share network ports unless network namespaces are separated. Loopback is not private to one user on a shared host. Linux network namespaces https://man7.org/linux/man-pages/man7/network namespaces.7.html Git worktrees separate working directories; they do not isolate services, databases, credentials, or resources. Allocate those intentionally before parallelizing agents. Git worktrees https://git-scm.com/docs/git-worktree The result I am pursuing is a workspace that is easier to return to, inspect, and steer across sessions. The device becomes one way into that work. I also built Local Clipboard , a Mac app that lets me paste my Mac clipboard into Claude sessions on the remote VM. Comment “CLIPBOARD” if you’d like a walkthrough in the next post. Follow for more practical developer workflows. How I Built a Persistent Remote Workspace for Developers and Coding Agents https://pub.towardsai.net/how-i-built-a-persistent-remote-workspace-for-developers-and-coding-agents-cd103ca2c341 was originally published in Towards AI https://pub.towardsai.net on Medium, where people are continuing the conversation by highlighting and responding to this story.