How to Run OpenWork in Headless Web Mode Without Electron for CI/CD Environments

Use pnpm dev:headless-web --detach to launch OpenWork's UI in a regular browser with a local openwork-server backend, bypassing Electron entirely for automated CI/CD pipelines.

Running OpenWork in headless web mode eliminates the Electron desktop dependency, making it ideal for continuous integration environments where graphical displays are unavailable. This guide explains the complete setup based on the implementation in the different-ai/openwork repository.

What Is Headless Web Mode?

Headless web mode runs OpenWork's Vite-powered UI in a standard browser while a local openwork-server process provides the backend API. According to the OpenWork source code, this architecture avoids Electron's Chromium shell entirely—critical for Docker containers, GitHub Actions, and other headless CI runners.

The mode is controlled by scripts/dev-headless-web.ts, which orchestrates process spawning, health checks, and graceful shutdown handling.

Quick Start for CI/CD Pipelines

Basic Detached Launch


# Install dependencies

pnpm install

# Start in detached mode (survives the spawning shell)

pnpm dev:headless-web --detach

The launcher outputs URLs like:


[dev:headless-web] Web URL: http://127.0.0.1:5178
[dev:headless-web] OpenWork server: http://127.0.0.1:8778

Store these URLs in environment variables for downstream test steps.

Complete CI Workflow Example

#!/bin/bash
set -e

# 1. Dependencies

pnpm install

# 2. Launch headless web stack

pnpm dev:headless-web --detach

# 3. Extract endpoints from runtime manifest

WEB_URL=$(cat tmp/dev-headless-web.json | jq -r .url)
API_URL=$(cat tmp/dev-headless-web.json | jq -r .apiUrl)

# 4. Run tests against the live UI

pnpm test:e2e --base-url="$WEB_URL"

How the Launcher Works Internally

The scripts/dev-headless-web.ts script performs eight key operations when you invoke pnpm dev:headless-web:

1. Detect Existing Instances

Reads tmp/dev-headless-web.json to check for healthy running stacks before deciding to reuse or replace them L71-L90.

2. Optional Detachment

With --detach, respawns itself in a new process group so the stack outlives the CI agent's job step L113-L139.

3. Dynamic Port Selection

Resolves free ports using OPENWORK_WEB_PORT (default 5178) and OPENWORK_PORT (default 8778), falling back to available ports if defaults are occupied L166-L187.

4. Secure Token Rotation

Generates fresh bearer tokens unless --keep-tokens is specified, preventing credential leakage between CI runs L207-L226.

5. Isolated Server Configuration

scripts/dev-headless-web-lib.ts merges workspace roots into tmp/headless-server.json instead of polluting the global config, ensuring workspaces survive --replace restarts L84-L130.

6. Process Spawning

Launches the Vite dev server and openwork-server with arguments built in dev-headless-web-lib.ts L139-L162.

7. Runtime Manifest Creation

Writes tmp/dev-headless-web.json with mode 0600 (private permissions), containing:

  • url: Vite UI endpoint
  • apiUrl: openwork-server endpoint
  • tokens: Bearer credentials
  • pids: Process IDs for cleanup
  • Optional Den proxy settings

L250-L272

8. Graceful Shutdown

Installs SIGINT and SIGTERM handlers to terminate child processes cleanly with logged exit reasons L311-L334.

Environment Variable Configuration

Override defaults in your CI configuration:

Variable Purpose Default
OPENWORK_WEB_PORT Vite dev server port 5178
OPENWORK_PORT openwork-server port 8778
OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY Enable Den Cloud proxy 1
OPENWORK_DEV_MODE Set automatically by npm script 1

GitHub Actions Example

name: OpenWork E2E Tests

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      
      - name: Start OpenWork headless web
        run: pnpm dev:headless-web --detach
        env:
          OPENWORK_WEB_PORT: 5179
          OPENWORK_PORT: 8780
          OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY: "0"
      
      - name: Run E2E tests
        run: pnpm test:e2e

Authentication in Headless Environments

The Den Cloud proxy enables same-origin authentication flows when browser redirects aren't feasible. Set OPENWORK_DEV_HEADLESS_WEB_DEN_PROXY=1 (default) to use copy-paste handoff instead of OAuth redirects—essential for containerized CI runners where callback URLs cannot reach the browser README.md – Headless web section.

Cleanup Strategies

Manual Process Termination


# Kill the detached launcher and its children

kill $(cat tmp/dev-headless-web.json | jq .pids.launcher)

Automatic CI Cleanup

Most CI runners terminate all processes when the job completes. For persistent runners, add a teardown step:

- name: Stop OpenWork
  if: always()
  run: |
    [ -f tmp/dev-headless-web.json ] && \
      kill $(jq .pids.launcher tmp/dev-headless-web.json) || true

Key Source Files Reference

File Role
scripts/dev-headless-web.ts Main launcher logic, signal handling, manifest I/O
scripts/dev-headless-web-lib.ts Token management, server arguments, config merging
package.json npm script definition (dev:headless-web)
tmp/dev-headless-web.json Runtime manifest (auto-generated)
tmp/headless-server.json Isolated server config (auto-generated)

Summary

  • pnpm dev:headless-web --detach starts OpenWork without Electron in CI-friendly detached mode.
  • The launcher in scripts/dev-headless-web.ts handles health checks, port selection, token rotation, and graceful shutdown automatically.
  • Runtime state is stored in tmp/dev-headless-web.json with restricted permissions for secure credential access.
  • Environment variables OPENWORK_WEB_PORT and OPENWORK_PORT customize network binding.
  • The Den proxy enables authentication flows in containerized environments where standard OAuth redirects fail.

Frequently Asked Questions

What command starts OpenWork without the Electron desktop window?

Run pnpm dev:headless-web to launch the UI in a regular browser with a local openwork-server backend. Add --detach for CI environments where the process must survive the spawning shell.

How do I access the URL and API endpoints in a CI script?

The launcher writes tmp/dev-headless-web.json containing url, apiUrl, and tokens. Use jq to extract values: cat tmp/dev-headless-web.json | jq -r .url.

Can I reuse an existing OpenWork instance between test runs?

Yes—the launcher detects healthy instances via tmp/dev-headless-web.json and reuses them unless you pass --replace. Use --keep-tokens to preserve authentication across restarts.

What if the default ports 5178 and 8778 are already occupied?

Set OPENWORK_WEB_PORT and OPENWORK_PORT environment variables, or let the launcher automatically find free ports and report the actual bindings in the runtime manifest.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →