| 1 | --- |
| 2 | title: Self-hosted runners |
| 3 | description: Run your workflow jobs, and if you choose your agents' work, on your own machines. Their time costs nothing. |
| 4 | --- |
| 5 | |
| 6 | A self-hosted runner is a machine of yours that runs your workflow jobs |
| 7 | instead of g1t's sandboxes. It can be a server, a Mac in a cupboard, a |
| 8 | Windows box with a GPU, or a pod in your cluster. On Linux you run |
| 9 | `g1t-runner` as a container under [Sysbox](#run-it-as-a-container), so |
| 10 | neither the runner nor its jobs can reach the machine; it registers once |
| 11 | and picks up jobs that ask for it with `runs-on: self-hosted`. |
| 12 | |
| 13 | - **Time on your runners is $0**, on every plan, including the free one. |
| 14 | It shows on [usage](/guides/usage-and-billing/) as self-hosted minutes. |
| 15 | - **It only connects out.** The runner polls `api.g1t.sh` over HTTPS for |
| 16 | work. Nothing needs to reach the machine: no open port, no tunnel. |
| 17 | - **Each job in a fresh sandbox.** Every job gets a container of its own, |
| 18 | made for it and removed after it, inside the runner's own container. |
| 19 | See [isolation](#isolation). |
| 20 | - **Any OS.** Linux, macOS and Windows, on x64 and arm64. On macOS and |
| 21 | Windows, sandboxed jobs are [not built yet](#windows-and-macos). |
| 22 | |
| 23 | | Where | Page | Who manages it | |
| 24 | | --- | --- | --- | |
| 25 | | A workspace | **Workspace → Runners** in the rail, `g1t.sh/<workspace>/-/runners` | Owners only. | |
| 26 | | A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository | |
| 27 | |
| 28 | A workspace's runners serve the repositories their [group](#groups) allows. |
| 29 | A project's own runners serve only that project. |
| 30 | |
| 31 | ## Run it as a container |
| 32 | |
| 33 | On a Linux machine, run the runner's image with the Sysbox runtime. The |
| 34 | runner starts a Docker Engine of its own inside its container and runs |
| 35 | each job in a container on that engine, so jobs are containers inside the |
| 36 | runner's container. Sysbox gives the runner's container a user namespace |
| 37 | of its own, so its root is an unprivileged user on the machine, and lets |
| 38 | it run Docker without `--privileged` and without the machine's Docker |
| 39 | socket. |
| 40 | |
| 41 | 1. Install [Sysbox](https://github.com/nestybox/sysbox) on the machine, |
| 42 | following its install guide. It needs Linux; Docker Desktop on macOS |
| 43 | and Windows cannot use it. |
| 44 | 2. Make a registration token: open **Runners** and click **New runner**. |
| 45 | 3. Run the runner: |
| 46 | |
| 47 | ```sh |
| 48 | docker run -d --name g1t-runner --restart unless-stopped \ |
| 49 | --runtime=sysbox-runc \ |
| 50 | -v g1t-runner:/data \ |
| 51 | -e G1T_RUNNER_URL=https://g1t.sh \ |
| 52 | -e G1T_RUNNER_TOKEN=g1trt_… \ |
| 53 | g1t.sh/flagon-io/g1t-runner |
| 54 | ``` |
| 55 | |
| 56 | The image runs `register-and-run`, which registers once and then runs. |
| 57 | Its configuration and credential are kept on the `g1t-runner` volume, so |
| 58 | the container can be restarted and replaced without registering again. |
| 59 | Every [register option](#register-options) can come from |
| 60 | `G1T_RUNNER_<OPTION>` in the environment, such as `G1T_RUNNER_LABELS=gpu` |
| 61 | or `G1T_RUNNER_MEMORY=8g`, so a secret can hold the token. |
| 62 | |
| 63 | Its log says which isolation it has: |
| 64 | |
| 65 | ```text |
| 66 | … Isolation: nested-container: each job runs in a fresh container on this runner's own Docker Engine, inside the runner's sandboxed container; neither reaches the machine. |
| 67 | ``` |
| 68 | |
| 69 | Do not give the container the machine's Docker socket |
| 70 | (`-v /var/run/docker.sock:/var/run/docker.sock`) or `--privileged`. The |
| 71 | runner looks for both when it starts, and refuses to start if it finds |
| 72 | them: |
| 73 | |
| 74 | | It finds | What it says | |
| 75 | | --- | --- | |
| 76 | | An engine's socket mounted in from the machine (`docker.sock`, `containerd.sock`, `podman.sock`, under any name) | *… is a container engine's socket mounted in from outside this container* | |
| 77 | | An engine's socket it did not make, such as from a mounted `/var/run` | *… is a container engine's socket that this runner did not make* | |
| 78 | | `DOCKER_HOST` set | *DOCKER_HOST is set …* | |
| 79 | | Root in the container is root on the machine (no Sysbox, or `--privileged`) | *root in this container is root on the machine* | |
| 80 | |
| 81 | To start anyway, with jobs able to reach the machine, set |
| 82 | `G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1`. The runner then logs each |
| 83 | problem with `UNSAFE:` in front, and registers as `container` rather than |
| 84 | `nested-container`. |
| 85 | |
| 86 | The engine's images and build cache are kept in the container, so they |
| 87 | last until the container is removed. Sysbox keeps them on the machine's |
| 88 | disk for that container alone; to keep them when you replace the |
| 89 | container, add `-v g1t-runner-docker:/var/lib/docker`, a volume for one |
| 90 | runner container at a time. |
| 91 | |
| 92 | ## Isolation |
| 93 | |
| 94 | How well a runner keeps work from its machine. A runner tells g1t its |
| 95 | level when it registers. |
| 96 | |
| 97 | | Isolation | How you get it | What a job can reach | |
| 98 | | --- | --- | --- | |
| 99 | | `nested-container` | The runner's image under Sysbox, as above, or [the desktop app](/guides/desktop/#how-jobs-run)'s Sandboxed choice. | Its own container, and its own Docker Engine if it uses Docker. Not the machine, and not other jobs. | |
| 100 | | `container` | `g1t-runner` installed on the machine with Docker, the desktop app's In Docker choice, or the image given the machine's Docker socket with the override. | Its own container. The runner itself drives the machine's Docker, which can do anything on the machine, so a job that escapes its container, or anything that takes over the runner, controls the machine. | |
| 101 | | `host` | `--no-docker`, or the desktop app's Directly on this computer choice. Deprecated. | Everything the runner's user can: the machine, its files, and what earlier jobs left. | |
| 102 | |
| 103 | ### What a job's container gets |
| 104 | |
| 105 | With `nested-container` and `container` isolation, each job runs in a |
| 106 | container made for it and removed after it: |
| 107 | |
| 108 | | | | |
| 109 | | --- | --- | |
| 110 | | Image | The job's `container:`, else the runner's `--image`, else `node:24-bookworm`. | |
| 111 | | Mounts | The harness (`g1t-runner`), read-only, and a volume of its own at `/home/runner`, removed after the job. Nothing else of the runner's or the machine's. | |
| 112 | | Network | A Docker network of its own, removed after the job. Never the machine's network. | |
| 113 | | Capabilities | Every one dropped but `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `FSETID`, `SETUID`, `SETGID`, `KILL` and `NET_BIND_SERVICE`, which installing packages and running test servers need. Raw sockets (`ping`) are among those dropped. | |
| 114 | | Privileges | `no-new-privileges`: a setuid program such as `sudo` gains nothing. Run steps as the image's root user rather than with `sudo`. Agent work keeps `sudo`, which its image uses once to put its guardrails out of the agent's reach. | |
| 115 | | Limits | `--memory` (three quarters of the runner's memory unless you set it, with no swap beyond it), `--cpus` (every CPU unless you set it) and `--pids-limit` (4096 unless you set it). | |
| 116 | | Credentials | Only the job's own short-lived token, passed by name. Never the runner's credential. | |
| 117 | |
| 118 | ### Docker in jobs |
| 119 | |
| 120 | With `nested-container` isolation, a job that uses Docker (its |
| 121 | `services:`, a Docker action, a `docker://` step, or `docker` in a step) |
| 122 | gets a Docker Engine of its own. It starts the first time the job uses |
| 123 | Docker, from `docker:28-dind`, beside the job's container and on the job's |
| 124 | network, so a service's published ports are on the job's `localhost`. It |
| 125 | is removed, with its images and containers, when the job ends. A job that |
| 126 | does not use Docker never starts one. |
| 127 | |
| 128 | The job's engine is privileged inside the runner's container. Sysbox keeps |
| 129 | it off the machine, but a job that uses Docker can reach the runner's own |
| 130 | container, the runner's credential included. For code you do not trust, |
| 131 | use [ephemeral runners](#autoscaling), so each job has a runner container |
| 132 | of its own, or register with `--no-job-docker`, which gives jobs no Docker. |
| 133 | |
| 134 | With `container` isolation, jobs have no Docker of their own, since it |
| 135 | would be the machine's: their `services:` are not started, and the log |
| 136 | says so. |
| 137 | |
| 138 | ### Host mode is deprecated |
| 139 | |
| 140 | `--no-docker` still works, and runs each job directly on the machine in a |
| 141 | folder of its own. It will be removed. Until then: |
| 142 | |
| 143 | - The runner's log says so when it starts: *Deprecated: this runner runs |
| 144 | jobs directly on this machine (--no-docker), unsandboxed.* |
| 145 | - Every job's log starts with a warning: *This job ran on the host, |
| 146 | unsandboxed …* |
| 147 | - It is not given a public repository's work, which anyone can propose |
| 148 | code to. The runner says how it is isolated when it registers and on |
| 149 | every poll, and g1t hands a public repository's jobs and agent work |
| 150 | only to runners that are not in host mode. A job no other online runner |
| 151 | could take waits, and its page says *Waiting for a sandboxed runner: |
| 152 | this repository is public, and host-mode runners don't run public |
| 153 | repositories' jobs.* It starts once a sandboxed runner with its labels |
| 154 | comes online. |
| 155 | - A runner from before this, which does not say how it is isolated, may |
| 156 | still be given such a job and refuses it itself: *acme/web is public, |
| 157 | and this self-hosted runner runs jobs directly on its machine |
| 158 | (--no-docker), unsandboxed, so it does not take a public repository's |
| 159 | work.* The job fails with that message. Update the runner. |
| 160 | |
| 161 | Move a Linux runner to [the container](#run-it-as-a-container). A macOS |
| 162 | or Windows runner stays in host mode until sandboxes for them are built. |
| 163 | |
| 164 | ### Windows and macOS |
| 165 | |
| 166 | Sandboxed jobs on Windows and macOS are not built yet; jobs native to |
| 167 | those systems run in host mode. What is planned: |
| 168 | |
| 169 | | | How each job will run | |
| 170 | | --- | --- | |
| 171 | | Windows | A Windows container with Hyper-V isolation: a small virtual machine with its own kernel, made from a signed image for the job and destroyed after it. | |
| 172 | | macOS | An ephemeral macOS virtual machine, using Apple's Virtualization framework, cloned from a signed image for the job and deleted after it. Apple's licence allows two macOS virtual machines per Mac at once, so a Mac will run at most two jobs at a time. | |
| 173 | |
| 174 | On both, the only part on the machine itself will be a small launcher that |
| 175 | makes and destroys those sandboxes from signed images; the runner and the |
| 176 | job run inside them. |
| 177 | |
| 178 | On a Mac or Windows machine with Docker Desktop, a runner installed there |
| 179 | without `--no-docker` already runs jobs in Linux containers, with |
| 180 | `container` isolation. |
| 181 | |
| 182 | ## The Runners page |
| 183 | |
| 184 | A workspace's **Runners** page shows g1t cloud and your own runners side |
| 185 | by side, with what each is running now and this month's time on each. |
| 186 | |
| 187 | | Part | What it shows | |
| 188 | | --- | --- | |
| 189 | | **g1t cloud** | How many agent runs and workflow jobs are running in g1t's sandboxes now, this month's sandbox time and what it came to at price, the price per minute from g1t's [price book](/guides/usage-and-billing/), and how many agent runs your plan allows at once. | |
| 190 | | **Your runners** | How many are online, busy, idle and offline, this month's self-hosted time (always $0), how many jobs and agent runs are waiting for one of them, and whether they take agent work. | |
| 191 | | **On g1t cloud now** | Each running agent run and workflow job, linked to its run, with its kind, project and when it started. Work handed to one of your runners is listed under that runner instead. | |
| 192 | | **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). | |
| 193 | | **Your runners** list | Each runner's status, labels, OS, architecture, `g1t-runner` version and when it was last heard from, and what it is running, linked to the workflow run or the agent run. | |
| 194 | | **Where work runs** | What decides where each kind of work runs, and the [agent setting](#agents-on-your-runners). | |
| 195 | |
| 196 | A runner is **Idle** when it is online with nothing to do, **Busy** while it |
| 197 | runs a job or an agent run, and **Offline** when it has not polled for 90 |
| 198 | seconds. When a service does not answer, only its part of the page says so. |
| 199 | |
| 200 | Sessions that persist are here for workspace agents: each has |
| 201 | [a computer of its own](/guides/agents/#its-computer) on g1t cloud, whose |
| 202 | home stays between sessions and which sleeps when idle; the page says so |
| 203 | under **Agents' computers and more**. Pinning an agent's computer to one of |
| 204 | your own runners and an official agent image are coming, and the page |
| 205 | labels them so. |
| 206 | |
| 207 | **Desktops** lists every computer in the workspace with [the desktop |
| 208 | app](/guides/desktop/#the-runner), whose runner runs while g1t is open |
| 209 | there: what each is doing, or why it isn't running and how to fix it, in |
| 210 | the app's own words, and how it runs jobs. A computer whose g1t has been |
| 211 | closed for ten minutes says *g1t is closed on* its name, and so does its |
| 212 | runner in the list, rather than *Offline*. |
| 213 | |
| 214 | ## A runner in the desktop app |
| 215 | |
| 216 | [The desktop app](/guides/desktop/#the-runner) runs a runner for you |
| 217 | while it is open, with nothing to install or register: it registers your |
| 218 | computer with the workspace you choose, as you, through the site, and |
| 219 | keeps the runner up to date. It runs jobs sandboxed by default |
| 220 | (`nested-container`); running them in Docker (`container`) or directly |
| 221 | on the computer (`host`) are settings you choose, and the app never falls |
| 222 | back to them. Its runner has the label `desktop` besides the usual ones. |
| 223 | |
| 224 | ## Add a runner |
| 225 | |
| 226 | On Linux, [run it as a container](#run-it-as-a-container). To install |
| 227 | `g1t-runner` on a machine instead, as on macOS and Windows: |
| 228 | |
| 229 | 1. Open **Runners** (a project's: **Settings → Runners**) and click **New runner**. g1t makes a |
| 230 | registration token and shows the commands for Linux, macOS, Windows and |
| 231 | Docker with it filled in. The token lasts an hour, can register any |
| 232 | number of runners until then, and is shown once. |
| 233 | 2. On the machine, download `g1t-runner` and register it: |
| 234 | |
| 235 | ```sh |
| 236 | # Linux (x64; use g1t-runner-linux-arm64 on arm64) |
| 237 | curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64 |
| 238 | chmod +x g1t-runner |
| 239 | ./g1t-runner register --url https://g1t.sh --token g1trt_… |
| 240 | ``` |
| 241 | |
| 242 | ```sh |
| 243 | # macOS (Apple silicon; use g1t-runner-macos-x64 on Intel) |
| 244 | curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64 |
| 245 | chmod +x g1t-runner |
| 246 | ./g1t-runner register --url https://g1t.sh --token g1trt_… |
| 247 | ``` |
| 248 | |
| 249 | ```powershell |
| 250 | # Windows (PowerShell) |
| 251 | Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe |
| 252 | .\g1t-runner.exe register --url https://g1t.sh --token g1trt_… |
| 253 | ``` |
| 254 | |
| 255 | 3. Start it: `./g1t-runner run` keeps it running in the terminal, or |
| 256 | install it as a service that starts with the machine: |
| 257 | |
| 258 | | OS | Command | What it makes | |
| 259 | | --- | --- | --- | |
| 260 | | Linux | `sudo ./g1t-runner service install` | A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>` | |
| 261 | | macOS | `./g1t-runner service install` | A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log` | |
| 262 | | Windows | `.\g1t-runner.exe service install`, from an Administrator prompt | A Windows service, `g1t-runner-<name>`, that restarts if it stops | |
| 263 | |
| 264 | `service start`, `stop`, `status` and `uninstall` do what they say. |
| 265 | |
| 266 | The runner shows up in the list within a few seconds, with a green dot. |
| 267 | |
| 268 | Installed this way, it has `container` isolation with Docker, or `host` |
| 269 | isolation with `--no-docker`. See [isolation](#isolation). |
| 270 | |
| 271 | ### Register options |
| 272 | |
| 273 | | Option | | |
| 274 | | --- | --- | |
| 275 | | `--url` | The g1t you register with: `https://g1t.sh`. | |
| 276 | | `--token` | The registration token. | |
| 277 | | `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). | |
| 278 | | `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). | |
| 279 | | `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. | |
| 280 | | `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). | |
| 281 | | `--no-docker` | Deprecated. Run jobs directly on the machine instead of in containers ([host mode](#host-mode-is-deprecated)). | |
| 282 | | `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. | |
| 283 | | `--agent-image` | The image [agent work](#agents-on-your-runners) runs in. | |
| 284 | | `--memory` | Each job container's memory, such as `8g`. Three quarters of the runner's memory if left out. | |
| 285 | | `--cpus` | Each job container's CPUs, such as `4` or `1.5`. Every CPU if left out. | |
| 286 | | `--pids-limit` | The most processes in a job container. 4096 if left out. | |
| 287 | | `--no-job-docker` | Give jobs no [Docker of their own](#docker-in-jobs). | |
| 288 | | `--job-docker-image` | The image a job's own Docker Engine runs. `docker:28-dind` if left out. | |
| 289 | | `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. | |
| 290 | | `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. | |
| 291 | | `--replace` | Take the place of a runner with the same name. | |
| 292 | | `--no-auto-update` | Never update itself. | |
| 293 | |
| 294 | To run several runners on one machine, give each its own `--dir` and |
| 295 | `--name`. |
| 296 | |
| 297 | ## Use it in a workflow |
| 298 | |
| 299 | A job runs on a self-hosted runner when its `runs-on` names `self-hosted`: |
| 300 | |
| 301 | ```yaml |
| 302 | jobs: |
| 303 | test: |
| 304 | runs-on: self-hosted |
| 305 | steps: |
| 306 | - uses: actions/checkout@v5 |
| 307 | - run: make test |
| 308 | |
| 309 | gpu: |
| 310 | runs-on: [self-hosted, linux, gpu] |
| 311 | steps: |
| 312 | - run: nvidia-smi |
| 313 | |
| 314 | windows: |
| 315 | runs-on: [self-hosted, windows] |
| 316 | steps: |
| 317 | - run: Get-ComputerInfo | Select-Object OsName |
| 318 | ``` |
| 319 | |
| 320 | Until a runner with every label the job names takes it, the job waits, and |
| 321 | its page says what for: *Waiting for a self-hosted runner with labels |
| 322 | self-hosted, linux, gpu.* A job that waits a day fails. Jobs that have |
| 323 | waited ten minutes with no matching runner online show under **Needs you** |
| 324 | on Mission control. |
| 325 | |
| 326 | `runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group |
| 327 | called GPU as well. |
| 328 | |
| 329 | ### Labels |
| 330 | |
| 331 | Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and |
| 332 | its architecture (`x64` or `arm64`), then the labels you gave it. A runner |
| 333 | that runs jobs in Docker is `linux`, whatever the machine is, since that is |
| 334 | what its jobs run on. Labels are matched without regard to case. |
| 335 | |
| 336 | A job takes a runner only when every label in its `runs-on` is one of the |
| 337 | runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or |
| 338 | `g1t-4core`, never go to a self-hosted runner. |
| 339 | |
| 340 | ### What a job gets |
| 341 | |
| 342 | A job on your runner runs exactly as it would in g1t's sandbox: the same |
| 343 | harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and |
| 344 | workflow commands, the same log and artifacts. `runner.name`, `runner.os` |
| 345 | and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is |
| 346 | `self-hosted`. |
| 347 | |
| 348 | - **In a container** (the default), each job gets a fresh container from |
| 349 | its `container:` image or the runner's `--image`, removed when it ends, |
| 350 | with the walls in [what a job's container gets](#what-a-jobs-container-gets). |
| 351 | In the runner's image under Sysbox, its `services:` and Docker actions |
| 352 | get [a Docker Engine of its own](#docker-in-jobs). |
| 353 | - **With `--no-docker`** (deprecated), each job gets a fresh folder under |
| 354 | the work folder, removed when it ends, and runs with whatever the |
| 355 | machine has installed. A step's default shell is `bash` on Linux and |
| 356 | macOS and PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`, |
| 357 | `bash` and `python` work where installed. On a machine with Docker, the |
| 358 | job's `services:`, `container:`, `docker://` steps and Docker actions |
| 359 | use the machine's Docker, as on GitHub's runners. |
| 360 | |
| 361 | A self-hosted job stops at 360 minutes unless its `timeout-minutes` says |
| 362 | otherwise, as on GitHub, and can ask for up to 24 hours (1440). Jobs on |
| 363 | g1t's own machines stop at 60 minutes. |
| 364 | |
| 365 | ## Groups |
| 366 | |
| 367 | A workspace's runners are in groups, which say which repositories may use |
| 368 | them. Every workspace has a **Default** group, for every repository, which |
| 369 | runners join unless their token or `--group` names another. |
| 370 | |
| 371 | To keep a set of machines for some repositories, open the workspace's |
| 372 | **Runners** page, click **New group**, name it, and choose **Only these** |
| 373 | repositories. Then click **New runner** with that group chosen, or register |
| 374 | with `--group`. Deleting a group moves its runners to the default group. |
| 375 | |
| 376 | ## Agents on your runners |
| 377 | |
| 378 | **Runners → Where work runs** can send g1t's own work to your |
| 379 | runners too: agent runs, checks, reviews, merge checks and the merge queue. |
| 380 | Switch on **Run g1t's work on self-hosted runners** and give the labels |
| 381 | a runner needs to take it (`self-hosted` is always one). |
| 382 | |
| 383 | - The agent works exactly as in g1t's sandbox: the same harness, with a |
| 384 | short-lived credential that can do only what that kind of run may, in |
| 385 | that repository, revoked when it ends. |
| 386 | - Its model calls still go through g1t's model proxy, `models.g1t.sh`, with |
| 387 | that credential, so budgets, caps and the audit log work as before. With |
| 388 | your own [model provider](/guides/models/), g1t charges nothing for the |
| 389 | run; otherwise the model is paid as usual and the machine is free. |
| 390 | - Agent work needs an image with git, Node, `tar` and the agent's CLI: |
| 391 | register the runner with `--agent-image` and an image of yours that has |
| 392 | them. Host mode on a Linux machine set aside for it, where `/work` can be |
| 393 | written, also works, and is deprecated. g1t does not publish an agent |
| 394 | image yet. |
| 395 | - [Guardrails](/guides/guardrails/) still apply to what the agent does, but |
| 396 | their network list cannot be enforced on your machine. The run's session |
| 397 | says so when it starts. |
| 398 | |
| 399 | A project can have its own setting, or follow the workspace's. |
| 400 | |
| 401 | ## What a run gets from Customize |
| 402 | |
| 403 | When g1t's work runs Claude Code on a runner (on g1t's own machines too), |
| 404 | the job gets what the workspace set up in [Customize](/guides/customize/), |
| 405 | as the job's agent would have it. Before Claude Code starts, the runner |
| 406 | reads the workspace's bundle with the job's short-lived agent credential, |
| 407 | once per job, and makes it ready: |
| 408 | |
| 409 | | In the bundle | What the runner does | In Claude Code | |
| 410 | | --- | --- | --- | |
| 411 | | Installed plugins that are switched on, each with where to fetch it and the commit it is pinned to | Fetches each at that commit (GitHub's archive of the commit, a clone of the repository here, or the zip the marketplace names) into `/work/.g1t/plugins/<name>/` and passes it with `--plugin-dir`, one flag per plugin | Its skills as `/<plugin>:<skill>`, its subagents as `<plugin>:<agent>`, and its hooks, LSP servers, output styles and `bin/`, which run only here and never on g1t's own agents | |
| 412 | | The library's own skills that reach the agent, as files | Writes them as one more plugin, `g1t-library`, with a `skills/<name>/` folder each, passed with `--plugin-dir` too. A plugin's skills come with the plugin itself | `/g1t-library:<skill>` | |
| 413 | | The connectors that reach the agent and are connected, with the headers that sign them in | Merges each into the `--mcp-config` file's `mcpServers`, by its name, beside g1t's own server | `mcp__<name>__<tool>` | |
| 414 | |
| 415 | The run's session gets one line saying what was loaded: *Loaded 2 plugins, |
| 416 | 1 library skill and 3 connectors from the workspace's Customize.* A plugin |
| 417 | that could not be fetched, or a bundle that could not be read, is named |
| 418 | there and the run goes on without it; nothing from Customize fails a run. |
| 419 | Plugins from GitHub need `tar` on the machine. |
| 420 | |
| 421 | A run on a fork's head loads none of the fork's own settings, hooks, MCP |
| 422 | servers or commands ([guardrails](/guides/guardrails/)). The workspace's |
| 423 | plugins and connectors are the workspace's, not the checkout's, so they are |
| 424 | still passed and merged; but every skill and command is switched off on |
| 425 | such a run, the plugins' and the library's included, so an agent cannot |
| 426 | invoke them there. |
| 427 | |
| 428 | The bundle carries the connectors' sign-ins, so only a job's agent |
| 429 | credential or a workspace's token reads it, never a person's: |
| 430 | [`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/), |
| 431 | scope `agents:run`. g1t never keeps or logs it, and the runner keeps the |
| 432 | keys it brought out of the session. See |
| 433 | [what a run gets](/guides/customize/#what-a-run-gets). |
| 434 | |
| 435 | ## Billing |
| 436 | |
| 437 | Time on your runners is free. Each job's minutes go on |
| 438 | [usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0, |
| 439 | so you can see how much ran there. |
| 440 | |
| 441 | - Workflow jobs on your runners need no plan and no card: they work on the |
| 442 | free tier. |
| 443 | - Agent work on your runners still needs its model paid for: the g1t plan, |
| 444 | the trial, the open-source pool on a public repository, or your own |
| 445 | model provider, which makes the run free. |
| 446 | |
| 447 | ## Security |
| 448 | |
| 449 | - **Pull requests from forks never run on your runners** unless you allow |
| 450 | it under **Runners → Where work runs**. Leave it off on a |
| 451 | public repository: anyone who can open a pull request could run any code |
| 452 | on the machine, read what it can reach, and leave something behind for |
| 453 | the next job. Such jobs get no secrets either way. |
| 454 | - **Secrets** reach a job on your runner under the same rules as on g1t's: |
| 455 | only trusted runs get them, and only the ones for the job's environment. |
| 456 | - **The runner's credential** can poll for work, report on what it was |
| 457 | given and remove itself, and nothing else; every other endpoint refuses |
| 458 | it. It is kept only as a hash on g1t's side, rotates every day, and stops |
| 459 | working the moment the runner is removed. It is never given to a job: |
| 460 | each job gets only its own short-lived token. |
| 461 | - **Isolation**: run the runner's image under Sysbox. Each job then gets |
| 462 | a container of its own inside the runner's container, removed after, |
| 463 | and neither reaches the machine. A job with `--no-docker` runs as the |
| 464 | runner's user and can reach whatever that user can; host mode is |
| 465 | deprecated and refuses public repositories' work. Use ephemeral runners |
| 466 | for code you do not trust. See [isolation](#isolation). |
| 467 | - **Network**: g1t's network guardrails are not enforced on your machines. |
| 468 | A job reaches whatever the machine can. |
| 469 | - **Audit**: making a registration token, registering, removing, and every |
| 470 | job a runner takes are in the [audit log](/guides/audit-log/), with the |
| 471 | runner as the actor. |
| 472 | - Only owners (or a repository's admins) register and remove runners, |
| 473 | signed in or with a person's token with `runners:admin`. Workspace |
| 474 | tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine |
| 475 | to run its own jobs. |
| 476 | |
| 477 | ## Docker and Kubernetes |
| 478 | |
| 479 | [Run it as a container](#run-it-as-a-container) covers Docker. The image's |
| 480 | settings, besides the register options: |
| 481 | |
| 482 | | Variable | | |
| 483 | | --- | --- | |
| 484 | | `G1T_RUNNER_SANDBOX` | `nested` in the image: start a Docker Engine inside the container and run jobs on it. | |
| 485 | | `G1T_RUNNER_DIR` | `/data` in the image: where the configuration and credential are kept. | |
| 486 | | `G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS` | `1` starts the runner even when it can reach the machine. See [above](#run-it-as-a-container). | |
| 487 | |
| 488 | A runner that ran the image with the machine's Docker socket, as these |
| 489 | docs said before, refuses to start with the new image. Remove the |
| 490 | `-v /var/run/docker.sock:/var/run/docker.sock` and add |
| 491 | `--runtime=sysbox-runc`; the `/data` volume keeps its registration. |
| 492 | |
| 493 | ### Autoscaling |
| 494 | |
| 495 | Ephemeral runners take one job and remove themselves, so each job starts in |
| 496 | a fresh runner container. In Kubernetes with Sysbox installed on the |
| 497 | nodes (its Kubernetes install adds the `sysbox-runc` runtime class), run |
| 498 | them in pods that make their own registration token from an owner's |
| 499 | personal [access token](/guides/authentication/#access-tokens) with only |
| 500 | `runners:admin`, and let the Deployment replace each pod that finishes: |
| 501 | |
| 502 | ```yaml |
| 503 | apiVersion: apps/v1 |
| 504 | kind: Deployment |
| 505 | metadata: |
| 506 | name: g1t-runner |
| 507 | spec: |
| 508 | replicas: 3 |
| 509 | selector: { matchLabels: { app: g1t-runner } } |
| 510 | template: |
| 511 | metadata: |
| 512 | labels: { app: g1t-runner } |
| 513 | annotations: { io.kubernetes.cri-o.userns-mode: "auto:size=65536" } |
| 514 | spec: |
| 515 | runtimeClassName: sysbox-runc |
| 516 | containers: |
| 517 | - name: runner |
| 518 | image: g1t.sh/flagon-io/g1t-runner |
| 519 | command: ["sh", "-c"] |
| 520 | args: |
| 521 | - | |
| 522 | export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \ |
| 523 | -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \ |
| 524 | https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \ |
| 525 | | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')" |
| 526 | exec g1t-runner register-and-run |
| 527 | env: |
| 528 | - { name: G1T_RUNNER_URL, value: https://g1t.sh } |
| 529 | - { name: G1T_RUNNER_LABELS, value: k8s } |
| 530 | - { name: G1T_RUNNER_EPHEMERAL, value: "1" } |
| 531 | - name: G1T_ADMIN_TOKEN |
| 532 | valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } } |
| 533 | ``` |
| 534 | |
| 535 | To scale with demand rather than keep a fixed number, change `replicas` |
| 536 | with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/) |
| 537 | says which are busy. |
| 538 | |
| 539 | ## Updates |
| 540 | |
| 541 | The runner checks for a new release every six hours while it is idle, and |
| 542 | updates itself when the release is signed by g1t's release key and its |
| 543 | download matches the release's SHA-256. `g1t-runner update` does it now; |
| 544 | `--no-auto-update` turns it off. Releases are at |
| 545 | `https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file. |
| 546 | |
| 547 | ## Remove a runner |
| 548 | |
| 549 | On the machine, `g1t-runner remove` unregisters it and forgets its |
| 550 | credential (run `service uninstall` first if it is a service). Or remove it |
| 551 | from the **Runners** page; the runner stops on its next poll. A job it was |
| 552 | running fails. Runners offline for 14 days are removed by themselves. |
| 553 | |
| 554 | ## API and MCP |
| 555 | |
| 556 | | What | REST | MCP (`workflow` tool) | Scope | |
| 557 | | --- | --- | --- | --- | |
| 558 | | List runners | [`GET /workspaces/{workspace}/actions/runners`](/reference/api/runners/list-runners-for-workspace/), [`GET /repos/{owner}/{name}/actions/runners`](/reference/api/runners/list-runners/) | `list_runners` | `runners:read` | |
| 559 | | Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` | |
| 560 | | Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` | |
| 561 | | Groups | [`GET`](/reference/api/runners/list-runner-groups/), [`POST`](/reference/api/runners/create-runner-group/), [`PATCH`](/reference/api/runners/update-runner-group/), [`DELETE`](/reference/api/runners/delete-runner-group/) `/workspaces/{workspace}/actions/runner-groups` | `list_runner_groups`, `create_runner_group`, `update_runner_group`, `delete_runner_group` | `runners:read`, `runners:admin` | |
| 562 | | Where work runs | [`GET`](/reference/api/runners/get-runner-settings-for-workspace/), [`PATCH`](/reference/api/runners/update-runner-settings-for-workspace/) `…/actions/runner-settings` | `get_runner_settings`, `update_runner_settings` | `runners:read`, `runners:admin` | |
| 563 | | What a run gets from Customize | [`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/) | `customize` tool, `bundle` | `agents:run`, with a job's agent token | |
| 564 | |
| 565 | The [Agent preset](/guides/authentication/#scopes) does not include |
| 566 | `runners:read`. |
| 567 | |
| 568 | ## Troubleshooting |
| 569 | |
| 570 | | What you see | What to do | |
| 571 | | --- | --- | |
| 572 | | The job says *Waiting for a self-hosted runner with labels …* | No online runner has every one of those labels in a group that allows the repository. Compare the labels on the **Runners** page, check the runner's group, or start the runner. | |
| 573 | | *Pull requests from forks do not run on self-hosted runners here.* | The run is from a fork. Allow it under **Where work runs**, if you trust everyone who can open a pull request. | |
| 574 | | `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. | |
| 575 | | `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. | |
| 576 | | `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. | |
| 577 | | *this runner is not sandboxed from its machine, so it will not take work* | The runner's container can reach the machine: it lists why. Remove the machine's Docker socket and `--privileged`, and run it with `--runtime=sysbox-runc`. See [run it as a container](#run-it-as-a-container). | |
| 578 | | *the runner's own Docker Engine could not start* | The container is not running under Sysbox. Add `--runtime=sysbox-runc`, and check that Sysbox is installed (`docker info` lists `sysbox-runc` under runtimes). | |
| 579 | | The job says *Waiting for a sandboxed runner: this repository is public …* | Every online runner with the job's labels is in host mode, which never runs a public repository's jobs. Run a runner's image under Sysbox, or register one without `--no-docker`. | |
| 580 | | *… is public, and this self-hosted runner runs jobs directly on its machine* | An older runner in host mode refused a public repository's job. Update it, so g1t gives the job to a sandboxed runner instead, and run the runner's image under Sysbox. | |
| 581 | | A step's `sudo` fails | Job containers have `no-new-privileges`. Steps run as the image's user, root in most CI images, so drop the `sudo`. | |
| 582 | | *could not run docker* | Install Docker and start it, and give the runner's user access to it. | |
| 583 | | Agent work fails at once | Give the runner `--agent-image`. | |
| 584 | | The runner is offline in the list | It has not polled for 90 seconds. Check its log (`journalctl -u g1t-runner-<name>`, `~/.g1t-runner/runner.log`, or the terminal) and that the machine can reach `https://api.g1t.sh`. | |