Coding Agent Runner
Windshift can run AI coding agents. Assign a work item to an agent. Windshift checks out the repository, runs the agent in an ephemeral container, pushes the run branch, and opens a draft pull request.
How execution works. The Windshift server orchestrates each run. It prepares and dispatches the run, but never starts an agent container. A separate windshift-runner claims work from a runner pool. Its Docker daemon starts one fresh agent container for each job, pushes the result branch, and reports the result.
SCM, LLM, and secret credentials stay on the Windshift server. Windshift brokers them to the agent at run time. The runner never receives them.
You can place runners in two ways:
- On the same host as Windshift: Run
windshift-runnerbeside Windshift in one Compose file. Connect both services through an internal Docker network. See Run a runner on the same host. This setup uses one machine and keeps agent execution out of the Windshift process. - On separate runner hosts: Use one or more dedicated machines. Docker and untrusted code then stay off the Windshift server, and you can scale across a pool.
Run a runner as a systemd service or Docker container. In both cases, it starts one fresh sandboxed agent container for each 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 does not need Docker.
- A Git connection in the workspace so the agent can clone and push a repository.
- An LLM connection in the workspace if the agent uses a managed model.
- An agent binding in the workspace. A run starts when a work item is assigned to the bound agent or when the agent is @mentioned in a comment. You can also attach a skills library and custom instructions. See Coding Agents for workspace settings.
Set up a runner
Every runner host joins a runner pool and pulls work from Windshift. The simplest setup runs one runner on the same host. See Run a runner on the same host. Use dedicated runner hosts to keep Docker and untrusted code off the Windshift server or to scale execution.
The runner creates the egress network. It starts each agent container in a dedicated Docker network,
coding-agent-egressby default. If the network does not exist, the runner creates it on first start. A plain bridge does not filter egress, so the runner logs a warning. The same-host Compose setup uses aninternalnetwork instead. It needs no firewall rules.
Run a runner as either:
- A Docker container that talks to the host Docker daemon.
- A systemd service on the host.
In both cases, the runner uses the host Docker daemon to start windshift-agent containers. The runner is not the agent. It claims jobs, prepares checkouts, starts containers, pushes result branches, and reports results.
1. Create a runner pool and registration token
In Windshift, an admin creates a runner pool and mints a registration token (wsrt_…). The token is a one-time bootstrap secret. On first start, the runner exchanges it for a per-instance credential (wsrc_…). The runner then stores and reuses that credential after restarts.
Use one registration token for each runner. For immutable deployments, inject an existing per-instance credential with WSRUNNER_CREDENTIAL.
2. Run the runner with Docker
This is the simplest remote-runner setup. The runner container uses the mounted socket to ask the host Docker daemon 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=disableSELinux hosts (Fedora / RHEL / CentOS Stream): read this before first start. SELinux blocks runner writes to
/var/lib/windshift-runner, even when the container runs as root. The failure ispermission denied. The audit log shows the actual SELinux denial.The runner can still register, but it cannot save its per-instance credential. Registration tokens are single-use. The first start consumes the token, the credential is lost on restart, and registration then fails with 401 until you mint a new token.
Run the container with
security_opt: ["label=disable"]in Compose or--security-opt label=disablewithdocker run. The usual:zvolume relabel is not sufficient. The runner also needs the Docker socket, and relabeling/var/run/docker.sockaffects the host daemon. Socket access still fails at the first job claim.Disabling label confinement for this container is the standard Docker-out-of-Docker setup on SELinux hosts. The agent containers keep their own sandbox with a read-only root, dropped capabilities, and restricted network.
Start the runner:
docker compose up -dThe agent is not a long-running service. The runner starts a fresh sandboxed agent container for each job. The host pulls WSRUNNER_IMAGE when it first needs it.
The
/var/lib/windshift-runnermount must use the same absolute path on the host and in the runner container. The host Docker daemon bind-mounts each prepared checkout into the agent container at/workspace.
Podman also works. With rootful Podman, point the socket mount to the Podman socket:
volumes:
- /run/podman/podman.sock:/var/run/docker.sock
- /var/lib/windshift-runner:/var/lib/windshift-runnerRootless Podman also works but needs extra care. It remaps user IDs, so the agent /workspace mount can have permission issues. Rootful Podman is simpler.
Run a runner on the same host
When the runner shares a host with Dockerized Windshift, put both services in one Compose file. Connect them through an internal Docker network. Agent containers use the same network.
An internal network has no route outside the host. Agents and the runner can reach Windshift, which brokers LLM and git traffic. They cannot reach anything else. Docker network isolation is the egress policy. You do not need a firewalld or iptables allowlist. Runner traffic does not use the 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 in this order:
- Start Windshift:
docker compose up -d windshift. - Mint the pool registration token in the admin runner-pool view.
- Add it to
.envasWSRUNNER_REGISTRATION_TOKEN. - Start the remaining services:
docker compose up -d.
Notes:
WSRUNNER_ALLOW_INSECURE=1is safe only in this setup. Control-plane traffic stays on the host 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 runner can control the host Docker daemon. This setup trades the isolation of a dedicated runner host for convenience. Windshift does not access Docker. Only the runner does.
3. Or install the runner as a systemd service
Use the systemd installation when you prefer native host processes or do not want to containerize the runner.
Each host needs:
- Linux with systemd
- Docker
git- Outbound HTTPS to your Windshift server
Download windshift-runner, windshift-triage, and the runner installer from your Windshift release. Then run:
sudo ./install.sh --bin-dir ./distThe installer creates a systemd service, its user, and its working directory. It installs windshift-runner and windshift-triage. It writes the 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:latestThen 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 -fThe runner registers and saves its per-instance credential. It then claims and runs jobs from 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 or 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. It does not need the token after it stores the credential. |
WSRUNNER_CREDENTIAL |
optional | - | Inject an existing per-instance runner credential (wsrc_…) instead of registering with a token. Use this 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
- SCM and LLM credentials stay on the Windshift server. Remote runners authenticate with a per-instance credential and per-run tokens. Windshift brokers inject provider credentials server-side.
- The agent container has no raw provider credentials. It uses short-lived run tokens and broker URLs.
- 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 push only its own branch (
agent-runs/run-<id>). The git proxy permits pushes only to the granted ref. - Docker access is high privilege. The runner controls the Docker daemon on its host, which effectively grants root access. Use dedicated disposable runner hosts. The Windshift server does not need Docker access.
- Registration tokens are single-use. Remove
WSRUNNER_REGISTRATION_TOKENafter first bootstrap when your deployment process permits it. The runner uses its persisted per-instance credential after restart.
Troubleshooting
- Runs never start: Confirm that the pool exists and
WSRUNNER_IMAGEis set. Make sure the runner Docker daemon can pull the agent image. - Runner says
WS_API_URL must be https://: Use an HTTPS URL that ends in/api. Or setWSRUNNER_ALLOW_INSECURE=1only for local development or same-host Compose. - Agent cannot reach Windshift: Check
WS_API_URLand runner outbound connectivity. If agent containers cannot reachBASE_URL, setCODING_AGENT_WS_API_URLon the Windshift server.localhostinside a container is not the Windshift server. - Permission denied talking to Docker: Make sure the runner process or container can access the Docker daemon.
- Remote git prep or push fails: Confirm that
windshift-triageandgitare available. Make sure the runner can reachWS_API_URLover HTTPS. - Runs fail with
prepare checkout: ... setup askpass: stat /tmp: no such file or directory: The runner host or container has no usable/tmp. Mounttmpfs: ["/tmp:exec,size=64M"]on the runner. The runner uses this location for git scratch space. Withoutexec, git cannot execute the askpass credential helper. See The /tmp tmpfs mount. - Runner asks for a registration token after restart: Make sure
WSRUNNER_CREDENTIAL_FILEis writable and persistent. In Docker, mount/var/lib/windshift-runneras a volume. - Runner logs
warning: could not persist credential ... permission deniedand gets 401 after restart (Fedora/RHEL/CentOS): SELinux blocked writes to/var/lib/windshift-runner, even when the container runs as root. Recreate the container with--security-opt label=disable. Then mint a new registration token because the previous registration consumed the old token without saving the credential. The lease reaper auto-revokes the orphaned instance.
See Environment Variables for the full configuration reference. See Coding Agents to connect editor agents such as Claude Code or Cursor to the ws CLI.