Skip to content
389 linesCodeBlameRaw
1---
2title: Self-hosted runners
3description: Run your workflow jobs, and if you choose your agents' work, on your own machines. Their time costs nothing.
4---
5
6A self-hosted runner is a machine of yours that runs your workflow jobs
7instead of g1t's sandboxes. It can be a server, a Mac in a cupboard, a
8Windows box with a GPU, or a pod in your cluster. You install `g1t-runner`
9on it, register it once, and it picks up jobs that ask for it with
10`runs-on: self-hosted`.
11
12- **Time on your runners is $0**, on every plan, including the free one.
13 It shows on [usage](/guides/usage-and-billing/) as self-hosted minutes.
14- **It only connects out.** The runner polls `api.g1t.sh` over HTTPS for
15 work. Nothing needs to reach the machine: no open port, no tunnel.
16- **Any OS.** Linux, macOS and Windows, on x64 and arm64. Jobs run in a
17 Docker container by default, or directly on the machine.
18
19| Where | Page | Who manages it |
20| --- | --- | --- |
21| A workspace | **Workspace → Runners** in the rail, `g1t.sh/<workspace>/-/runners` | Owners only. |
22| A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository |
23
24A workspace's runners serve the repositories their [group](#groups) allows.
25A project's own runners serve only that project.
26
27## The Runners page
28
29A workspace's **Runners** page shows g1t cloud and your own runners side
30by side, with what each is running now and this month's time on each.
31
32| Part | What it shows |
33| --- | --- |
34| **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. |
35| **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. |
36| **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. |
37| **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). |
38| **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. |
39| **Where work runs** | What decides where each kind of work runs, and the [agent setting](#agents-on-your-runners). |
40
41A runner is **Idle** when it is online with nothing to do, **Busy** while it
42runs a job or an agent run, and **Offline** when it has not polled for 90
43seconds. When a service does not answer, only its part of the page says so.
44
45Sessions that persist are here for workspace agents: each has
46[a computer of its own](/guides/agents/#its-computer) on g1t cloud, whose
47home stays between sessions and which sleeps when idle; the page says so
48under **Agents' computers and more**. Pinning an agent's computer to one of
49your own runners, a runner in the desktop app, and an official agent image
50are coming, and the page labels them so.
51
52## Add a runner
53
541. Open **Runners** (a project's: **Settings → Runners**) and click **New runner**. g1t makes a
55 registration token and shows the commands for Linux, macOS, Windows and
56 Docker with it filled in. The token lasts an hour, can register any
57 number of runners until then, and is shown once.
582. On the machine, download `g1t-runner` and register it:
59
60 ```sh
61 # Linux (x64; use g1t-runner-linux-arm64 on arm64)
62 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64
63 chmod +x g1t-runner
64 ./g1t-runner register --url https://g1t.sh --token g1trt_…
65 ```
66
67 ```sh
68 # macOS (Apple silicon; use g1t-runner-macos-x64 on Intel)
69 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64
70 chmod +x g1t-runner
71 ./g1t-runner register --url https://g1t.sh --token g1trt_…
72 ```
73
74 ```powershell
75 # Windows (PowerShell)
76 Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe
77 .\g1t-runner.exe register --url https://g1t.sh --token g1trt_…
78 ```
79
803. Start it: `./g1t-runner run` keeps it running in the terminal, or
81 install it as a service that starts with the machine:
82
83 | OS | Command | What it makes |
84 | --- | --- | --- |
85 | Linux | `sudo ./g1t-runner service install` | A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>` |
86 | macOS | `./g1t-runner service install` | A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log` |
87 | Windows | `.\g1t-runner.exe service install`, from an Administrator prompt | A Windows service, `g1t-runner-<name>`, that restarts if it stops |
88
89 `service start`, `stop`, `status` and `uninstall` do what they say.
90
91The runner shows up in the list within a few seconds, with a green dot.
92
93### Register options
94
95| Option | |
96| --- | --- |
97| `--url` | The g1t you register with: `https://g1t.sh`. |
98| `--token` | The registration token. |
99| `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). |
100| `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). |
101| `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. |
102| `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). |
103| `--no-docker` | Run jobs directly on the machine instead of in containers. |
104| `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. |
105| `--agent-image` | The image [agent work](#agents-on-your-runners) runs in. |
106| `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. |
107| `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. |
108| `--replace` | Take the place of a runner with the same name. |
109| `--no-auto-update` | Never update itself. |
110
111To run several runners on one machine, give each its own `--dir` and
112`--name`.
113
114## Use it in a workflow
115
116A job runs on a self-hosted runner when its `runs-on` names `self-hosted`:
117
118```yaml
119jobs:
120 test:
121 runs-on: self-hosted
122 steps:
123 - uses: actions/checkout@v5
124 - run: make test
125
126 gpu:
127 runs-on: [self-hosted, linux, gpu]
128 steps:
129 - run: nvidia-smi
130
131 windows:
132 runs-on: [self-hosted, windows]
133 steps:
134 - run: Get-ComputerInfo | Select-Object OsName
135```
136
137Until a runner with every label the job names takes it, the job waits, and
138its page says what for: *Waiting for a self-hosted runner with labels
139self-hosted, linux, gpu.* A job that waits a day fails. Jobs that have
140waited ten minutes with no matching runner online show under **Needs you**
141on Mission control.
142
143`runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group
144called GPU as well.
145
146### Labels
147
148Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and
149its architecture (`x64` or `arm64`), then the labels you gave it. A runner
150that runs jobs in Docker is `linux`, whatever the machine is, since that is
151what its jobs run on. Labels are matched without regard to case.
152
153A job takes a runner only when every label in its `runs-on` is one of the
154runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or
155`g1t-4core`, never go to a self-hosted runner.
156
157### What a job gets
158
159A job on your runner runs exactly as it would in g1t's sandbox: the same
160harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and
161workflow commands, the same log and artifacts. `runner.name`, `runner.os`
162and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is
163`self-hosted`.
164
165- **In Docker** (the default), each job gets a fresh container from its
166 `container:` image or the runner's `--image`, removed when it ends. The
167 runner needs Docker, and its user needs to be allowed to use it. Its
168 `services:` are not started, since the job's container has no Docker of
169 its own; the log says so.
170- **With `--no-docker`**, each job gets a fresh folder under the work
171 folder, removed when it ends, and runs with whatever the machine has
172 installed. A step's default shell is `bash` on Linux and macOS and
173 PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`, `bash` and
174 `python` work where installed. On a machine with Docker, the job's
175 `services:`, `container:`, `docker://` steps and Docker actions use
176 the machine's Docker, as on GitHub's runners.
177
178A self-hosted job stops at 60 minutes unless its `timeout-minutes` says
179more, up to 24 hours (1440). Jobs on g1t's own machines stop at 60 minutes.
180
181## Groups
182
183A workspace's runners are in groups, which say which repositories may use
184them. Every workspace has a **Default** group, for every repository, which
185runners join unless their token or `--group` names another.
186
187To keep a set of machines for some repositories, open the workspace's
188**Runners** page, click **New group**, name it, and choose **Only these**
189repositories. Then click **New runner** with that group chosen, or register
190with `--group`. Deleting a group moves its runners to the default group.
191
192## Agents on your runners
193
194**Runners → Where work runs** can send g1t's own work to your
195runners too: agent runs, checks, reviews, merge checks and the merge queue.
196Switch on **Run g1t's work on self-hosted runners** and give the labels
197a runner needs to take it (`self-hosted` is always one).
198
199- The agent works exactly as in g1t's sandbox: the same harness, with a
200 short-lived credential that can do only what that kind of run may, in
201 that repository, revoked when it ends.
202- Its model calls still go through g1t's model proxy, `models.g1t.sh`, with
203 that credential, so budgets, caps and the audit log work as before. With
204 your own [model provider](/guides/models/), g1t charges nothing for the
205 run; otherwise the model is paid as usual and the machine is free.
206- Agent work needs an image with git, Node, `tar` and the agent's CLI:
207 register the runner with `--agent-image` and an image of yours that has
208 them, or run it with `--no-docker` on a Linux machine set aside for it,
209 where `/work` can be written. g1t does not publish an agent image yet.
210- [Guardrails](/guides/guardrails/) still apply to what the agent does, but
211 their network list cannot be enforced on your machine. The run's session
212 says so when it starts.
213
214A project can have its own setting, or follow the workspace's.
215
216## What a run gets from Customize
217
218When g1t's work runs Claude Code on a runner (on g1t's own machines too),
219the job gets what the workspace set up in [Customize](/guides/customize/),
220as the job's agent would have it. Before Claude Code starts, the runner
221reads the workspace's bundle with the job's short-lived agent credential,
222once per job, and makes it ready:
223
224| In the bundle | What the runner does | In Claude Code |
225| --- | --- | --- |
226| 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 |
227| 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>` |
228| 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>` |
229
230The run's session gets one line saying what was loaded: *Loaded 2 plugins,
2311 library skill and 3 connectors from the workspace's Customize.* A plugin
232that could not be fetched, or a bundle that could not be read, is named
233there and the run goes on without it; nothing from Customize fails a run.
234Plugins from GitHub need `tar` on the machine.
235
236A run on a fork's head loads none of the fork's own settings, hooks, MCP
237servers or commands ([guardrails](/guides/guardrails/)). The workspace's
238plugins and connectors are the workspace's, not the checkout's, so they are
239still passed and merged; but every skill and command is switched off on
240such a run, the plugins' and the library's included, so an agent cannot
241invoke them there.
242
243The bundle carries the connectors' sign-ins, so only a job's agent
244credential or a workspace's token reads it, never a person's:
245[`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/),
246scope `agents:run`. g1t never keeps or logs it, and the runner keeps the
247keys it brought out of the session. See
248[what a run gets](/guides/customize/#what-a-run-gets).
249
250## Billing
251
252Time on your runners is free. Each job's minutes go on
253[usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0,
254so you can see how much ran there.
255
256- Workflow jobs on your runners need no plan and no card: they work on the
257 free tier.
258- Agent work on your runners still needs its model paid for: the g1t plan,
259 the trial, the open-source pool on a public repository, or your own
260 model provider, which makes the run free.
261
262## Security
263
264- **Pull requests from forks never run on your runners** unless you allow
265 it under **Runners → Where work runs**. Leave it off on a
266 public repository: anyone who can open a pull request could run any code
267 on the machine, read what it can reach, and leave something behind for
268 the next job. Such jobs get no secrets either way.
269- **Secrets** reach a job on your runner under the same rules as on g1t's:
270 only trusted runs get them, and only the ones for the job's environment.
271- **The runner's credential** can poll for work, report on what it was
272 given and remove itself, and nothing else; every other endpoint refuses
273 it. It is kept only as a hash on g1t's side, rotates every day, and stops
274 working the moment the runner is removed. It is never given to a job:
275 each job gets only its own short-lived token.
276- **Isolation**: a job in Docker gets a container of its own, removed after.
277 A job with `--no-docker` runs as the runner's user, in a folder of its
278 own; it can reach whatever that user can. Use a dedicated account, and
279 ephemeral runners for untrusted code.
280- **Network**: g1t's network guardrails are not enforced on your machines.
281 A job reaches whatever the machine can.
282- **Audit**: making a registration token, registering, removing, and every
283 job a runner takes are in the [audit log](/guides/audit-log/), with the
284 runner as the actor.
285- Only owners (or a repository's admins) register and remove runners,
286 signed in or with a person's token with `runners:admin`. Workspace
287 tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine
288 to run its own jobs.
289
290## Docker and Kubernetes
291
292The runner's image runs `register-and-run`, which registers once and then
293runs. Each option can also come from `G1T_RUNNER_<OPTION>` in the
294environment, such as `G1T_RUNNER_TOKEN` and `G1T_RUNNER_LABELS`, so a
295secret can hold the token:
296
297```sh
298docker run -d --name g1t-runner --restart unless-stopped \
299 -v /var/run/docker.sock:/var/run/docker.sock \
300 -v g1t-runner:/data -e G1T_RUNNER_DIR=/data \
301 g1t.sh/flagon-io/g1t-runner register-and-run --url https://g1t.sh --token g1trt_…
302```
303
304With the host's Docker socket, each job runs in a sibling container.
305
306### Autoscaling
307
308Ephemeral runners take one job and remove themselves, so each job starts on
309a clean machine. In Kubernetes, run them with `--no-docker` in pods that
310make their own registration token from an owner's personal
311[access token](/guides/authentication/#access-tokens) with only
312`runners:admin`, and let the Deployment replace each pod that finishes:
313
314```yaml
315apiVersion: apps/v1
316kind: Deployment
317metadata:
318 name: g1t-runner
319spec:
320 replicas: 3
321 selector: { matchLabels: { app: g1t-runner } }
322 template:
323 metadata: { labels: { app: g1t-runner } }
324 spec:
325 containers:
326 - name: runner
327 image: g1t.sh/flagon-io/g1t-runner
328 command: ["sh", "-c"]
329 args:
330 - |
331 export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \
332 -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \
333 https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \
334 | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
335 exec g1t-runner register-and-run
336 env:
337 - { name: G1T_RUNNER_URL, value: https://g1t.sh }
338 - { name: G1T_RUNNER_LABELS, value: k8s }
339 - { name: G1T_RUNNER_EPHEMERAL, value: "1" }
340 - { name: G1T_RUNNER_NO_DOCKER, value: "1" }
341 - name: G1T_ADMIN_TOKEN
342 valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } }
343```
344
345To scale with demand rather than keep a fixed number, change `replicas`
346with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/)
347says which are busy.
348
349## Updates
350
351The runner checks for a new release every six hours while it is idle, and
352updates itself when the release is signed by g1t's release key and its
353download matches the release's SHA-256. `g1t-runner update` does it now;
354`--no-auto-update` turns it off. Releases are at
355`https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file.
356
357## Remove a runner
358
359On the machine, `g1t-runner remove` unregisters it and forgets its
360credential (run `service uninstall` first if it is a service). Or remove it
361from the **Runners** page; the runner stops on its next poll. A job it was
362running fails. Runners offline for 14 days are removed by themselves.
363
364## API and MCP
365
366| What | REST | MCP (`workflow` tool) | Scope |
367| --- | --- | --- | --- |
368| 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` |
369| Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` |
370| Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
371| 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` |
372| 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` |
373| 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 |
374
375The [Agent preset](/guides/authentication/#scopes) does not include
376`runners:read`.
377
378## Troubleshooting
379
380| What you see | What to do |
381| --- | --- |
382| 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. |
383| *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. |
384| `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. |
385| `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. |
386| `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. |
387| *could not run docker* | Install Docker and start it, give the runner's user access to it, or register with `--no-docker`. |
388| Agent work fails at once | Give the runner `--agent-image`, or run it on Linux with `--no-docker`. |
389| 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`. |