Coding Agent Runner

Windshift can run AI coding agents for you. Assign a work item to an agent and Windshift checks out the repository, runs the agent in a throwaway container, pushes the run branch, and opens a draft pull request with the result.

How execution works. When you enable coding agents, the Windshift server acts as an orchestrator: it prepares each run and dispatches it, but it never runs an agent container itself. Agents always execute on a windshift-runner, a separate process that claims work from a runner pool, starts one fresh agent container per job on its Docker daemon, pushes the result branch, and reports back. Your SCM, LLM, and secret credentials stay on the Windshift server and are brokered to the agent at run time, so they never reach the runner.

You can place runners two ways:

  • On the same host as Windshift: the simplest setup. Run the windshift-runner alongside Windshift in one Compose file, connected by an internal Docker network (see Run a runner on the same host). This keeps agent execution out of the Windshift process while needing only one machine.
  • On separate runner hosts: offload execution to one or more dedicated machines, so Docker and untrusted code stay off your Windshift server and you can scale out across a pool.

A runner can be installed as a systemd service or run as a Docker container. In both modes it starts one fresh, sandboxed agent container per job.

This guide assumes you have already installed and are running Windshift.

Before you start

You need:

  • A Docker daemon on the runner host. Agent jobs run in ephemeral containers. The Windshift server itself does not need Docker.
  • A Git connection configured in your workspace, so the agent has a repo to clone and push to.
  • An LLM connection exposed to the workspace if the agent should use a managed model connection.
  • An agent binding in the workspace. A run starts when a work item is assigned to the bound agent user, or when the agent is @mentioned in a comment on an item. You can also attach a skills library and custom instructions to a binding. See Coding Agents in the user guide for those workspace-level settings.

Set up a runner

Every runner host joins a runner pool and pulls work from Windshift. The quickest setup runs a single runner on the same host as Windshift (see Run a runner on the same host). Add dedicated runner hosts when you want Docker and untrusted code off the Windshift server, or when you want to scale execution across several machines.

The egress network is created for you. The runner spawns each agent container into a dedicated Docker network (coding-agent-egress by default) and creates that network on first start if it does not exist. Because a plain bridge does not filter egress, the runner logs a loud warning pointing at the egress-filtering docs. The same-host Compose setup below replaces that warning with an internal network, which needs no firewall rules at all.

A runner can run either:

  • as a Docker container that talks to the host Docker daemon; or
  • as a systemd service installed on the host.

In both cases the runner uses the host Docker daemon to start the per-job windshift-agent containers. The runner itself is not the agent; it claims jobs, prepares the checkout, starts the agent container, pushes the result branch, and reports the result.

1. Create a runner pool and registration token

In Windshift, an admin creates a runner pool and mints a registration token (wsrt_…). Registration tokens are one-time bootstrap secrets. On first start, the runner exchanges the token for a per-instance credential (wsrc_…) and then stores/reuses that credential for future restarts.

Use one registration token per runner instance. For immutable deployments you can also inject an existing per-instance credential with WSRUNNER_CREDENTIAL.

2. Run the runner with Docker

This is the easiest remote-runner setup. The runner container talks to the host Docker daemon through the mounted socket and asks it to start sibling agent containers.

services:
  windshift-runner:
    image: ghcr.io/windshiftapp/windshift-runner:latest
    restart: unless-stopped
    environment:
      WS_API_URL: https://windshift.example.com/api
      WSRUNNER_REGISTRATION_TOKEN: wsrt_your_one_time_token
      WSRUNNER_IMAGE: ghcr.io/windshiftapp/windshift-agent:latest
      # Optional display name shown in Windshift:
      # WSRUNNER_NAME: runner-1
    volumes:
      # Lets the runner start sibling agent containers on the host daemon.
      - /var/run/docker.sock:/var/run/docker.sock
      # Must be the same path on host and in the runner container.
      - /var/lib/windshift-runner:/var/lib/windshift-runner
    # Required on SELinux-enforcing hosts (Fedora, RHEL, CentOS Stream) —
    # see the SELinux note below before first start.
    # security_opt:
    #   - label=disable

SELinux hosts (Fedora / RHEL / CentOS Stream): read this before the first start. With SELinux enforcing, the runner's writes to /var/lib/windshift-runner are blocked even though the container runs as root — the failure is a plain permission denied, with the real denial only visible in the audit log. The runner still registers successfully, but cannot persist its per-instance credential. Because registration tokens are single-use, that combination burns the token: the first start consumes it, the credential is lost on restart, and re-registration fails with 401 until you mint a new token.

Run the container with security_opt: ["label=disable"] (compose) or --security-opt label=disable (docker run). The usual :z volume relabel is not sufficient here: the runner also needs the Docker socket, and /var/run/docker.sock cannot be relabeled without affecting the host daemon — socket access would still be denied at the first job claim. Disabling label confinement for this one container is the standard Docker-out-of-Docker setup on SELinux hosts; the agent containers the runner spawns are unaffected and keep their own sandbox (read-only root, dropped capabilities, restricted network).

Then start it:

docker compose up -d

The agent itself is not a long-running service. The runner starts a fresh, sandboxed agent container for each job and pulls WSRUNNER_IMAGE on the host the first time it is needed.

The /var/lib/windshift-runner mount must resolve to the same absolute path on the host and inside the runner container. The host Docker daemon — not the runner container — bind-mounts each prepared checkout into the agent container as /workspace.

Podman works too. With rootful Podman, point the socket mount at the Podman socket instead:

    volumes:
      - /run/podman/podman.sock:/var/run/docker.sock
      - /var/lib/windshift-runner:/var/lib/windshift-runner

Rootless Podman also works but needs extra care: because it remaps user IDs, the agent's /workspace mount can hit permission issues unless you account for the mapping. Rootful Podman is the smoother path.

Run a runner on the same host

When the runner runs on the same host as a dockerized Windshift, put both services in one Compose file and connect them through an internal Docker network. Agent containers are spawned into that same network, and because an internal network has no route out of the host, agents and the runner can reach Windshift — which brokers all LLM and git traffic — and nothing else. Docker's network isolation is the egress policy: no firewalld or iptables allowlist to set up, nothing to re-apply when the API host's IP changes, and runner traffic never takes a round trip through your public URL.

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    ports:
      - "8080:8080"
    tmpfs:
      - /tmp:exec,size=64M
    environment:
      - BASE_URL=https://windshift.example.com
      - SSO_SECRET=${SSO_SECRET}
      # In-network API address; the broker URLs handed to agent containers
      # resolve on the internal network below. Must end in /api.
      - CODING_AGENT_WS_API_URL=http://windshift:8080/api
    volumes:
      - windshift-data:/data
    networks:
      - default
      - coding-agent
    restart: unless-stopped

  windshift-runner:
    image: ghcr.io/windshiftapp/windshift-runner:latest
    environment:
      # In-Compose control plane. Plaintext HTTP is acceptable only because
      # this traffic never leaves the host's Docker bridge.
      WS_API_URL: http://windshift:8080/api
      WSRUNNER_ALLOW_INSECURE: "1"
      WSRUNNER_REGISTRATION_TOKEN: ${WSRUNNER_REGISTRATION_TOKEN}
      WSRUNNER_IMAGE: ghcr.io/windshiftapp/windshift-agent:latest
    volumes:
      # Lets the runner start sibling agent containers on the host daemon.
      - /var/run/docker.sock:/var/run/docker.sock
      # Must be the same path on host and in the runner container.
      - /var/lib/windshift-runner:/var/lib/windshift-runner
    networks:
      - coding-agent
    depends_on:
      - windshift
    restart: unless-stopped
    # SELinux-enforcing hosts: see the SELinux note above.
    # security_opt:
    #   - label=disable

networks:
  # Agent containers are spawned into this network — the name matches the
  # runner's default. "internal: true" means containers on it can reach each
  # other but nothing outside the host.
  coding-agent:
    name: coding-agent-egress
    internal: true

volumes:
  windshift-data:

Bootstrap order: start Windshift first (docker compose up -d windshift), mint the pool registration token in the admin runner-pool view, add it to .env as WSRUNNER_REGISTRATION_TOKEN, then docker compose up -d.

Notes:

  • WSRUNNER_ALLOW_INSECURE=1 is safe only in this shape: the control-plane traffic stays on the host's Docker bridge. Never use it across hosts.
  • The internal network does not affect image pulls — the host Docker daemon pulls WSRUNNER_IMAGE, not the runner container.
  • The same warning about Docker access applies: the runner can control the host's Docker daemon, so this setup trades the isolation of a dedicated runner host for convenience. Windshift itself still stays free of Docker access; only the runner talks to the Docker daemon.

3. Or install the runner as a systemd service

Use the systemd install when you prefer native host processes or do not want the runner itself containerized.

Each host needs:

  • Linux with systemd
  • Docker
  • git
  • Outbound HTTPS to your Windshift server

Download the windshift-runner and windshift-triage binaries plus the runner installer from your Windshift release, then run:

sudo ./install.sh --bin-dir ./dist

The installer sets up the runner as a systemd service, creates its user and working directory, installs windshift-runner and windshift-triage, and writes a configuration file at /etc/windshift-runner/runner.env.

Edit /etc/windshift-runner/runner.env:

WS_API_URL=https://windshift.example.com/api
WSRUNNER_REGISTRATION_TOKEN=wsrt_your_one_time_token
WSRUNNER_IMAGE=ghcr.io/windshiftapp/windshift-agent:latest

Then pull the agent image and start the service:

docker pull ghcr.io/windshiftapp/windshift-agent:latest
sudo systemctl start windshift-runner
journalctl -u windshift-runner -f

You should see the runner register, persist its per-instance credential, then claim and run jobs assigned to its pool.

Runner configuration reference

Variable Required Default Purpose
WS_API_URL Windshift API base URL ending in /api (for example https://host/api). The runner control plane and brokers live here; this is not /rest/api/v1 and not the bare host URL. HTTPS is required unless WSRUNNER_ALLOW_INSECURE=1 is set for development.
WSRUNNER_REGISTRATION_TOKEN first bootstrap One-time pool registration token (wsrt_…). The runner exchanges it for a per-instance credential and does not need it again after the credential is stored.
WSRUNNER_CREDENTIAL optional Inject an existing per-instance runner credential (wsrc_…) instead of registering with a token. Useful for immutable deployments.
WSRUNNER_CREDENTIAL_FILE optional <cache>/credential Path where the runner stores/reuses its per-instance credential.
WSRUNNER_IMAGE Agent container image to run, usually ghcr.io/windshiftapp/windshift-agent:latest.
WSRUNNER_NAME hostname Name shown for this runner.
WSRUNNER_DOCKER docker Docker-compatible CLI to invoke.
WSRUNNER_TRIAGE_BIN windshift-triage Path to the triage helper used for git prepare/push.
WSRUNNER_CACHE_ROOT /var/lib/windshift-runner/cache Host-local bare-clone cache. Keep it under the same-path bind mount when running the runner in Docker.
WSRUNNER_POLL_INTERVAL 2s How often to check for work when idle.
WSRUNNER_HEARTBEAT_INTERVAL 30s How often to renew the runner lease.
WSRUNNER_INITIAL_PROMPT server prompt Emergency fallback only; normal jobs receive the server-managed prompt in the claim.
WSRUNNER_ALLOW_INSECURE unset Set to 1 to allow a plaintext http:// WS_API_URL. Only for local development, or for the same-host Compose setup where runner traffic never leaves the host's Docker bridge.
WSRUNNER_ALLOW_UNLABELED_IMAGE unset Set to 1 to accept an agent image that lacks the agent-contract label. The runner otherwise refuses to start an unlabeled image. Use only for custom images you build yourself.

Security

  • Your SCM and LLM credentials stay on the Windshift server. Remote runners authenticate with a per-instance credential and per-run tokens. Git and LLM access flow through Windshift brokers, which inject the real provider credentials server-side.
  • The agent container has no raw provider credentials. It uses short-lived run tokens and broker URLs instead.
  • Each run is sandboxed. Agents run as a non-root user in a container with a read-only root filesystem, dropped capabilities, tmpfs scratch space, and the configured Docker network.
  • A run can only push its own branch (agent-runs/run-<id>). The git proxy gates pushes to the run's single granted ref.
  • Docker access is powerful. The runner process controls the Docker daemon on its host, which is effectively root on that host. Run runner hosts as dedicated, disposable machines. The Windshift server never needs Docker access.
  • Registration tokens are single-use. Remove WSRUNNER_REGISTRATION_TOKEN after first bootstrap if your deployment process allows it; the persisted per-instance credential is what the runner uses on restart.

Troubleshooting

  • Runs never start: confirm the pool exists and WSRUNNER_IMAGE is set, and that the agent image can be pulled by the runner's Docker daemon.
  • Runner says WS_API_URL must be https://: use an HTTPS URL ending in /api, or set WSRUNNER_ALLOW_INSECURE=1 (only for local development or the same-host Compose setup, where traffic stays on the Docker bridge).
  • Agent can't reach Windshift: check WS_API_URL and outbound connectivity from the runner. If BASE_URL is not reachable from agent containers, set CODING_AGENT_WS_API_URL on the Windshift server. localhost inside a container is not your Windshift server.
  • Permission denied talking to Docker: the runner process or container must be able to access the Docker daemon.
  • Remote git prep/push fails: confirm windshift-triage is installed (or present in the runner image), git is available, and the runner can reach WS_API_URL over HTTPS.
  • Runs fail with prepare checkout: ... setup askpass: stat /tmp: no such file or directory: the runner host or container is missing a usable /tmp; the runner uses it as git scratch space. Mount tmpfs: ["/tmp:exec,size=64M"] on the runner. If /tmp is mounted but without exec, the same git steps fail with a permission error executing the askpass credential helper instead, because Docker tmpfs mounts are noexec by default. See The /tmp tmpfs mount for details.
  • Runner keeps asking for a registration token after restart: check that WSRUNNER_CREDENTIAL_FILE is writable and persisted. In Docker, keep /var/lib/windshift-runner mounted as a volume.
  • Runner logs warning: could not persist credential ... permission denied and gets 401 after a restart (Fedora/RHEL/CentOS): SELinux blocked the write to /var/lib/windshift-runner even though the container runs as root. Recreate the container with --security-opt label=disable (see the SELinux note in the Docker runner section), then mint a new registration token, because the old one was consumed by the registration whose credential could not be saved. The orphaned instance is auto-revoked by the lease reaper.

See Environment Variables for the full configuration reference, and Coding Agents for wiring editor agents like Claude Code or Cursor to the ws CLI.