Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Commands that differ by system sit in tabs, one per system, and a choice holds across pages. Quickstart's token is set for bash on macOS and Linux and for PowerShell on Windows, and the self-hosted runners guide (now MDX) downloads, registers and installs the runner as a service in macOS, Linux and Windows tabs, where three blocks and a table stood one after another. Tabs are drawn like the docs' others, quiet labels over a lavender line, and a page's Markdown copy shows each tab under its label in bold. The home page opens with what its subtitle doesn't already say, and its cards carry icons (LinkCard's icon).

syntaqxcommitted Parente4d73edBrowse files
7 files+857−7780/7 viewed
+16−2
11 ---
2−// Starlight's <LinkCard>, with Lucide's arrow.
2+// Starlight's <LinkCard>, with Lucide's arrow, and optionally a Lucide icon
3+// (its name on lucide.dev) in a lavender tile above the title.
34 import type { HTMLAttributes } from 'astro/types';
45
56 import LucideIcon from './LucideIcon.astro';
78 interface Props extends Omit<HTMLAttributes<'a'>, 'title'> {
89 title: string;
910 description?: string;
11+ icon?: string;
1012 }
1113
12−const { title, description, ...attributes } = Astro.props;
14+const { title, description, icon, ...attributes } = Astro.props;
1315 ---
1416
1517 <div class="sl-link-card">
1618 <span class="sl-flex stack">
19+ {icon && <span class="tile"><LucideIcon name={icon} size="1.125rem" /></span>}
1720 <a {...attributes}>
1821 <span class="title" set:html={title} />
1922 </a>
4851 flex-direction: column;
4952 gap: 0.5rem;
5053 }
54+ .tile {
55+ display: inline-flex;
56+ align-items: center;
57+ justify-content: center;
58+ width: 2.25rem;
59+ height: 2.25rem;
60+ margin-bottom: 0.25rem;
61+ border-radius: 0.6rem;
62+ background: var(--g1t-accent-low);
63+ color: var(--g1t-accent);
64+ }
5165 .title {
5266 color: var(--sl-color-white);
5367 font-weight: 600;
+0−773
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 your own
21− computer, `g1t-runner sandbox up` runs Linux jobs in a Linux VM of the
22− runner's own and Windows jobs each in a Windows Sandbox: see
23− [sandbox on your computer](#sandbox-on-your-computer).
24−
25−| Where | Page | Who manages it |
26−| --- | --- | --- |
27−| A workspace | **Workspace → Runners** in the rail, `g1t.sh/<workspace>/-/runners` | Owners only. |
28−| A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository |
29−
30−A workspace's runners serve the repositories their [group](#groups) allows.
31−A project's own runners serve only that project.
32−
33−## Run it as a container
34−
35−On a Linux machine, run the runner's image with the Sysbox runtime. The
36−runner starts a Docker Engine of its own inside its container and runs
37−each job in a container on that engine, so jobs are containers inside the
38−runner's container. Sysbox gives the runner's container a user namespace
39−of its own, so its root is an unprivileged user on the machine, and lets
40−it run Docker without `--privileged` and without the machine's Docker
41−socket.
42−
43−1. Install [Sysbox](https://github.com/nestybox/sysbox) 0.7 or later on
44− the machine, following its install guide. Earlier versions cannot run
45− the runner's own Docker Engine, whose `runc` checks `/proc` in a way
46− they do not allow. Sysbox needs Linux; on a Mac or Windows computer,
47− use [the sandbox](#sandbox-on-your-computer) instead.
48−2. Make a registration token: open **Runners** and click **New runner**.
49−3. Run the runner:
50−
51− ```sh
52− docker run -d --name g1t-runner --restart unless-stopped \
53− --runtime=sysbox-runc \
54− -v g1t-runner:/data \
55− -e G1T_RUNNER_URL=https://g1t.sh \
56− -e G1T_RUNNER_TOKEN=g1trt_… \
57− g1t.sh/flagon-io/g1t-runner
58− ```
59−
60−The image runs `register-and-run`, which registers once and then runs.
61−Its configuration and credential are kept on the `g1t-runner` volume, so
62−the container can be restarted and replaced without registering again.
63−Every [register option](#register-options) can come from
64−`G1T_RUNNER_<OPTION>` in the environment, such as `G1T_RUNNER_LABELS=gpu`
65−or `G1T_RUNNER_MEMORY=8g`, so a secret can hold the token.
66−
67−Its log says which isolation it has:
68−
69−```text
70−… 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.
71−```
72−
73−Do not give the container the machine's Docker socket
74−(`-v /var/run/docker.sock:/var/run/docker.sock`) or `--privileged`. The
75−runner looks for both when it starts, and refuses to start if it finds
76−them:
77−
78−| It finds | What it says |
79−| --- | --- |
80−| 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* |
81−| 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* |
82−| `DOCKER_HOST` set | *DOCKER_HOST is set …* |
83−| Root in the container is root on the machine (no Sysbox, or `--privileged`) | *root in this container is root on the machine* |
84−
85−To start anyway, with jobs able to reach the machine, set
86−`G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1`. The runner then logs each
87−problem with `UNSAFE:` in front, and registers as `container` rather than
88−`nested-container`.
89−
90−The engine's images and build cache are kept in the container, so they
91−last until the container is removed. Sysbox keeps them on the machine's
92−disk for that container alone; to keep them when you replace the
93−container, add `-v g1t-runner-docker:/var/lib/docker`, a volume for one
94−runner container at a time.
95−
96−## Isolation
97−
98−How well a runner keeps work from its machine. A runner tells g1t its
99−level when it registers and on every poll, and the **Runners** page shows
100−it on each runner: *Sandboxed*, *VM per job*, *Shared Docker* or
101−*Unsandboxed*.
102−
103−| Isolation | How you get it | What a job can reach |
104−| --- | --- | --- |
105−| `nested-container` | The runner's image under Sysbox, as above; on your own computer, `g1t-runner sandbox up`, which runs the same inside a Linux VM of the runner's own; 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. |
106−| `vm` | `g1t-runner sandbox up` on Windows 11 Pro, Enterprise or Education, or `register --native` there: jobs native to Windows, each in a [Windows Sandbox](#windows-jobs-in-windows-sandbox). | Its own virtual machine, made for the job and discarded after, with a read-only copy of the harness and no other folder of the machine's. |
107−| `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. |
108−| `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. |
109−
110−### What a job's container gets
111−
112−With `nested-container` and `container` isolation, each job runs in a
113−container made for it and removed after it:
114−
115−| | |
116−| --- | --- |
117−| Image | The job's `container:`, else the runner's `--image`, else `node:24-bookworm`. |
118−| 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. |
119−| Network | A Docker network of its own, removed after the job. Never the machine's network. |
120−| 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. |
121−| 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. |
122−| 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). |
123−| Credentials | Only the job's own short-lived token, passed by name. Never the runner's credential. |
124−
125−### Docker in jobs
126−
127−With `nested-container` isolation, a job that uses Docker (its
128−`services:`, a Docker action, a `docker://` step, or `docker` in a step)
129−gets a Docker Engine of its own. It starts the first time the job uses
130−Docker, from `docker:28-dind`, beside the job's container and on the job's
131−network, so a service's published ports are on the job's `localhost`. It
132−is removed, with its images and containers, when the job ends. A job that
133−does not use Docker never starts one.
134−
135−The job's engine is privileged inside the runner's container. Sysbox keeps
136−it off the machine, but a job that uses Docker can reach the runner's own
137−container, the runner's credential included. For code you do not trust,
138−use [ephemeral runners](#autoscaling), so each job has a runner container
139−of its own, or register with `--no-job-docker`, which gives jobs no Docker.
140−
141−With `container` isolation, jobs have no Docker of their own, since it
142−would be the machine's: their `services:` are not started, and the log
143−says so.
144−
145−### Host mode is deprecated
146−
147−`--no-docker` still works, and runs each job directly on the machine in a
148−folder of its own. It will be removed. Until then:
149−
150−- The runner's log says so when it starts: *Deprecated: this runner runs
151− jobs directly on this machine (--no-docker), unsandboxed.*
152−- Every job's log starts with a warning: *This job ran on the host,
153− unsandboxed …*
154−- It is not given a public repository's work, which anyone can propose
155− code to. The runner says how it is isolated when it registers and on
156− every poll, and g1t hands a public repository's jobs and agent work
157− only to runners that are not in host mode. A job no other online runner
158− could take waits, and its page says *Waiting for a sandboxed runner:
159− this repository is public, and host-mode runners don't run public
160− repositories' jobs.* It starts once a sandboxed runner with its labels
161− comes online.
162−- A runner from before this, which does not say how it is isolated, may
163− still be given such a job and refuses it itself: *acme/web is public,
164− and this self-hosted runner runs jobs directly on its machine
165− (--no-docker), unsandboxed, so it does not take a public repository's
166− work.* The job fails with that message. Update the runner.
167−
168−Move a Linux runner to [the container](#run-it-as-a-container), and a
169−Windows or Mac computer to [the sandbox](#sandbox-on-your-computer). A
170−runner registers as running jobs native to Windows or macOS (the
171−`windows` or `macos` label) only when it sandboxes them (`--native`), or
172−when you chose host mode with `--no-docker`.
173−
174−## Sandbox on your computer
175−
176−`g1t-runner sandbox` sets up self-hosted runners on your own desktop or
177−laptop that nothing a job runs can get out of: not to your files, your
178−other programs, your processes or your Docker. It gives the computer up
179−to two capabilities, each a runner of its own on the **Runners** page:
180−
181−| Capability | Runner | Labels | Isolation |
182−| --- | --- | --- | --- |
183−| Linux jobs | `<name>-linux`, inside a Linux VM of the runner's own | `self-hosted`, `linux`, and the architecture | `nested-container` |
184−| Jobs native to the computer's OS | `<name>-windows`, on the computer, starting a Windows Sandbox per job | `self-hosted`, `windows`, and the architecture | `vm` |
185−
186−1. Make a registration token: open **Runners** and click **New runner**.
187−2. Download `g1t-runner` for your computer, as in [add a runner](#add-a-runner),
188− then run:
189−
190− ```sh
191− g1t-runner sandbox up --url https://g1t.sh --token g1trt_…
192− ```
193−
194−The first `up` fetches the sandbox's image for this release, checks its
195−signature and SHA-256, makes the VM, and registers both runners with the
196−token; it takes a few minutes. After that, `up` starts what is stopped
197−and needs no token.
198−
199−| Command | What it does |
200−| --- | --- |
201−| `g1t-runner sandbox up` | Makes the sandbox if there is none, and starts both runners. Waits until the Linux runner is asking for work. |
202−| `g1t-runner sandbox down` | Stops both now. A running job is cut short, and its run says the runner was stopped. `--drain` waits for it to finish instead. `--delete` also removes the VM and everything in it; the runners stay listed on g1t until you remove them there. |
203−| `g1t-runner sandbox down --only windows` | Stops Windows jobs alone and keeps them off: Linux jobs carry on, and a later `up` leaves Windows jobs stopped. `--only linux` does the same for Linux jobs, leaving Windows jobs running. Takes `--drain`, not `--delete`. |
204−| `g1t-runner sandbox up --only windows` | Turns Windows jobs on again and starts them, leaving Linux jobs as they are. `--only linux,windows` turns on both. |
205−| `g1t-runner sandbox status` | Says how each capability is, and what to do about one that is not available. |
206−| `g1t-runner sandbox update` | Drains the Linux runner (the job it is running finishes), swaps in the newest release's image, and starts it again. The registration is kept, so it is the same runner on g1t. |
207−
208−| Option (`up`) | |
209−| --- | --- |
210−| `--url`, `--token` | Where to register and the registration token, for the first `up`. The token can come from `G1T_RUNNER_TOKEN` in the environment instead, which keeps it out of the process list. |
211−| `--name` | The runners' name before `-linux` and `-windows`. The computer's name if left out. |
212−| `--labels`, `--group` | As for [register](#register-options), for both runners. |
213−| `--cpus` | The Linux VM's runner's CPUs, and so its jobs'. Half the computer's, at most 8, if left out. |
214−| `--memory` | Its memory, such as `8g`. A quarter of the computer's, between 2 and 8 GB and never more than half, if left out. |
215−| `--disk` | The VM's disk on macOS and Linux, such as `64g` (64 GB if left out). On Windows the disk is not limited: WSL can only resize it with all of WSL shut down. |
216−| `--only` | The kinds of job to start, of `linux`, `windows` and `macos` (`native` is this computer's own): turned on, and kept on. The others are left as they are, running or not. Without it, `up` starts every kind that isn't turned off. `down` takes it too. |
217−| `--no-native`, `--native` | Turn the native capability off, or on again, as `down --only` and `up --only` do. Kept until said otherwise. |
218−| `--state-dir` | Where the sandbox keeps its settings and images. `~/.g1t-runner/sandbox` if left out. Every command takes it. |
219−| `--json` | Print the status as JSON when done (see below). Every command takes it. |
220−
221−### How each computer runs it
222−
223−| Computer | Linux jobs | Native jobs |
224−| --- | --- | --- |
225−| Windows 11 Pro, Enterprise, Education | A WSL 2 distribution of the runner's own, `g1t-sandbox` | Windows Sandbox, one per job |
226−| Windows 11 Home | A WSL 2 distribution of the runner's own, `g1t-sandbox` | Not available: Home has no Windows Sandbox |
227−| macOS 13 or later | A VM under Apple's Virtualization framework, run by [vfkit](https://github.com/crc-org/vfkit) (`brew install vfkit`) | Not released: sandboxed macOS jobs are designed, not built |
228−| Linux with Sysbox 0.7+ | No VM: the runner's image under Sysbox, as [above](#run-it-as-a-container) | None (Linux jobs are native) |
229−| Linux without Sysbox | A VM under QEMU with KVM (`/dev/kvm`, and `qemu-system-x86_64` or `qemu-system-aarch64`) | None |
230−
231−Inside the VM (and on Linux with Sysbox) it is the runner's image under
232−Sysbox, exactly as in [run it as a container](#run-it-as-a-container):
233−each job is a fresh container on the runner's own Docker Engine, inside the
234−runner's Sysbox container, inside the VM. Nothing listens on your
235−computer; the VM only connects out.
236−
237−**What the VM gets of your computer:** no folders and no drives, no way
238−to start its programs, no Docker socket, no clipboard, no GPU, display or
239−sound, and no network port in. On Windows, the distribution's
240−`/etc/wsl.conf` turns off WSL's drive mounts, its interop with Windows
241−programs and Windows' `PATH`, and when it starts the sandbox also unmounts
242−what WSL puts in every distribution anyway: `/mnt/wsl` (shared by all
243−distributions, where Docker Desktop keeps sockets to its engine on
244−Windows), WSLg's display and sound sockets, and Windows' GPU driver
245−folders. It checks that nothing of the computer's is left, and refuses to
246−run jobs if anything is. Containers in the VM, every job among them, may
247−reach the internet but not your computer: a firewall rule in the VM drops
248−their traffic to the computer's address, but for DNS.
249−
250−**Resources:** the runner's container has `--cpus` and `--memory`, and
251−each job has its own limits inside that ([what a job's container gets](#what-a-jobs-container-gets)).
252−On Windows these are the sandbox's share of WSL's own VM, which
253−`%USERPROFILE%\.wslconfig` sizes for all of WSL.
254−
255−**Images and updates:** the sandbox's images are published with each
256−runner release, at `g1t.sh/downloads/runner/<version>/`, and named with
257−their SHA-256 in the release's manifest, which is signed with g1t's
258−release key like the runner itself (see [updates](#updates)). The runner's
259−image inside is named by digest in the same manifest. `update` checks the
260−signature and every SHA-256 before using anything, and waits for the
261−running job before it swaps the image. The runner's registration lives
262−apart from the image (on its own disk on macOS and Linux; on Windows the
263−launcher carries it across), so an update does not register again.
264−
265−### Windows jobs in Windows Sandbox
266−
267−With `vm` isolation, the runner on your computer takes jobs that ask for
268−`runs-on: [self-hosted, windows]` and runs each in a Windows Sandbox of
269−its own: a Hyper-V virtual machine with its own kernel, started fresh from
270−your Windows and discarded, disk and all, when the job ends.
271−
272−- Its only folder from your computer is read-only and holds a copy of the
273− runner (the job's harness) and the job's script. Nothing it writes
274− outlives it.
275−- No GPU, clipboard, printers, microphone or camera; networking on, so the
276− job can reach g1t and the internet.
277−- A firewall rule in it blocks the address your computer is reached by.
278− The job runs as an administrator in its sandbox and could remove it, so
279− it slows a job down rather than stopping it; the wall is the VM.
280−- One job at a time, as every runner. Agent work runs on Linux runners.
281−- It needs Windows 11 Pro, Enterprise or Education, version 24H2 or later
282− (for the `wsb` command), with **Windows Sandbox** turned on under
283− Settings, System, Optional features, More Windows features. `status`
284− says which of these is missing.
285−
286−On a runner that is not under `sandbox up`, `g1t-runner register --native`
287−does the same.
288−
289−### What it cannot stop
290−
291−- **WSL 2 distributions share one VM.** On Windows, every WSL 2
292− distribution, your own among them, runs in one virtual machine with one
293− Linux kernel, and root in one can reach the others' disks. That is why
294− nothing a job runs is root in the sandbox's distribution: the runner is
295− a Sysbox container whose root is an unprivileged user there, and each job
296− a container inside it. A job would have to escape its container and the
297− runner's, and then reach root in the distribution, before it could touch
298− your other distributions, from which Windows is reachable.
299−- **Mirrored networking.** With `networkingMode=mirrored` in
300− `.wslconfig`, Linux reaches Windows' own `localhost`, so the sandbox
301− will not start until it is removed.
302−- **The computer's network.** Jobs reach the internet, and so the other
303− machines on your network that the internet could. Only your computer's
304− own address is blocked.
305−- **The VM's own root** could reach your computer's services on its
306− address. Jobs are two containers away from it.
307−- **A Windows job's sandbox** is as strong as Windows Sandbox: a Hyper-V
308− VM, with one read-only folder of your computer's.
309−- **macOS and Linux VMs** (vfkit and QEMU) are built to the same design,
310− with no shared folders and nothing forwarded in, but have not yet been
311− run on a Mac or a Linux machine without Sysbox. Windows is where the
312− sandbox has been checked end to end.
313−
314−### The status as JSON
315−
316−`g1t-runner sandbox status --json` prints one JSON object, which [the
317−desktop app](/guides/desktop/) shows. It exits 0 whenever it could read
318−the status, a sandbox in error included; `up`, `down` and `update` print
319−the same object with `--json` when they are done, and exit 1 with
320−`error` set when they fail.
321−
322−```json
323−{
324− "schema": 1,
325− "state": "running",
326− "backend": "wsl2",
327− "runner_version": "0.3.0",
328− "launcher_version": "0.3.0",
329− "image_version": "0.3.0",
330− "isolation": "nested-container",
331− "job": { "kind": "workflow", "id": "job_…", "name": "build", "repo": "acme/web" },
332− "error": null,
333− "resources": { "cpus": 4, "memory_mib": 8192, "disk_gib": 64 },
334− "capabilities": [
335− { "id": "linux", "available": true, "state": "running", "isolation": "nested-container",
336− "runner_version": "0.3.0", "job": null, "cause": null, "fix": null, "turned_off": false },
337− { "id": "windows", "available": false, "state": "absent", "isolation": "vm",
338− "runner_version": null, "job": null, "turned_off": false,
339− "cause": "Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).",
340− "fix": "On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox under Settings, System, Optional features, More Windows features, then restart." }
341− ],
342− "state_dir": "C:\\Users\\you\\.g1t-runner\\sandbox"
343−}
344−```
345−
346−| Field | |
347−| --- | --- |
348−| `state` | The Linux VM's: `absent` (never made, or deleted), `stopped`, `starting`, `running`, `updating` or `error`. |
349−| `backend` | What holds it: `wsl2`, `vfkit`, `qemu` or `sysbox`. |
350−| `runner_version`, `image_version`, `launcher_version` | The runner inside the VM, the VM's image, and this `g1t-runner`. |
351−| `job` | What the Linux runner is running now, or null. |
352−| `error` | A sentence for a person when something is wrong, or null. |
353−| `capabilities` | Each kind of job the computer can run sandboxed: `linux`, and `windows` or `macos`. `available` says whether it can be had here; when not, `cause` says why and `fix` what to do. Each has its own `state`, `job` and `runner_version`. A native one whose runner didn't start is in `error`, with why in `cause`. |
354−| `turned_off` | On each capability: someone stopped it (`down --only`, `--no-native`). Its `state` is then `stopped`: stopped by a person, not failing, and nothing in `error`. |
355−
356−Fields are only added; `schema` changes if one changes meaning.
357−
358−## The Runners page
359−
360−A workspace's **Runners** page shows g1t cloud and your own runners side
361−by side, with what each is running now and this month's time on each.
362−
363−| Part | What it shows |
364−| --- | --- |
365−| **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. |
366−| **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. |
367−| **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. |
368−| **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). |
369−| **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. |
370−| **Where work runs** | What decides where each kind of work runs, and the [agent setting](#agents-on-your-runners). |
371−
372−A runner is **Idle** when it is online with nothing to do, **Busy** while it
373−runs a job or an agent run, and **Offline** when it has not polled for 90
374−seconds. When a service does not answer, only its part of the page says so.
375−
376−Sessions that persist are here for workspace agents: each has
377−[a computer of its own](/guides/agents/#its-computer) on g1t cloud, whose
378−home stays between sessions and which sleeps when idle; the page says so
379−under **Agents' computers and more**. Pinning an agent's computer to one of
380−your own runners and an official agent image are coming, and the page
381−labels them so.
382−
383−**Desktops** lists every computer in the workspace with [the desktop
384−app](/guides/desktop/#the-runner), whose runner runs while g1t is open
385−there: what each is doing, or why it isn't running and how to fix it, in
386−the app's own words, and how it runs jobs. A computer whose g1t has been
387−closed for ten minutes says *g1t is closed on* its name, and so does its
388−runner in the list, rather than *Offline*.
389−
390−## A runner in the desktop app
391−
392−[The desktop app](/guides/desktop/#the-runner) runs a runner for you
393−while it is open, with nothing to install or register: it registers your
394−computer with the workspace you choose, as you, through the site, and
395−keeps the runner up to date. It runs jobs sandboxed by default
396−(`nested-container`); running them in Docker (`container`) or directly
397−on the computer (`host`) are settings you choose, and the app never falls
398−back to them. Sandboxed, it is [the sandbox](#sandbox-on-your-computer),
399−which the app sets up with `g1t-runner sandbox up` and shows with
400−`sandbox status --json`: Linux jobs and, on Windows 11 Pro and up,
401−Windows jobs, each a runner of its own. Its runners have the label
402−`desktop` besides the usual ones.
403−
404−## Add a runner
405−
406−On Linux, [run it as a container](#run-it-as-a-container). To install
407−`g1t-runner` on a machine instead, as on macOS and Windows:
408−
409−1. Open **Runners** (a project's: **Settings → Runners**) and click **New runner**. g1t makes a
410− registration token and shows the commands for Linux, macOS, Windows and
411− Docker with it filled in. The token lasts an hour, can register any
412− number of runners until then, and is shown once.
413−2. On the machine, download `g1t-runner` and register it:
414−
415− ```sh
416− # Linux (x64; use g1t-runner-linux-arm64 on arm64)
417− curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64
418− chmod +x g1t-runner
419− ./g1t-runner register --url https://g1t.sh --token g1trt_…
420− ```
421−
422− ```sh
423− # macOS (Apple silicon; use g1t-runner-macos-x64 on Intel)
424− curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64
425− chmod +x g1t-runner
426− ./g1t-runner register --url https://g1t.sh --token g1trt_…
427− ```
428−
429− ```powershell
430− # Windows (PowerShell)
431− Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe
432− .\g1t-runner.exe register --url https://g1t.sh --token g1trt_…
433− ```
434−
435−3. Start it: `./g1t-runner run` keeps it running in the terminal, or
436− install it as a service that starts with the machine:
437−
438− | OS | Command | What it makes |
439− | --- | --- | --- |
440− | Linux | `sudo ./g1t-runner service install` | A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>` |
441− | macOS | `./g1t-runner service install` | A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log` |
442− | Windows | `.\g1t-runner.exe service install`, from an Administrator prompt | A Windows service, `g1t-runner-<name>`, that restarts if it stops |
443−
444− `service start`, `stop`, `status` and `uninstall` do what they say.
445−
446−The runner shows up in the list within a few seconds, with a green dot.
447−
448−Installed this way, it has `container` isolation with Docker, or `host`
449−isolation with `--no-docker`. See [isolation](#isolation).
450−
451−### Register options
452−
453−| Option | |
454−| --- | --- |
455−| `--url` | The g1t you register with: `https://g1t.sh`. |
456−| `--token` | The registration token. |
457−| `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). |
458−| `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). |
459−| `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. |
460−| `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). |
461−| `--no-docker` | Deprecated. Run jobs directly on the machine instead of in containers ([host mode](#host-mode-is-deprecated)). |
462−| `--native` | Run jobs native to this OS, each in a throwaway VM of its own: [Windows Sandbox](#windows-jobs-in-windows-sandbox) on Windows. Registers with the OS's label and `vm` isolation, and refuses if the sandbox is not there. |
463−| `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. |
464−| `--agent-image` | The image [agent work](#agents-on-your-runners) runs in. |
465−| `--memory` | Each job container's memory, such as `8g`. Three quarters of the runner's memory if left out. |
466−| `--cpus` | Each job container's CPUs, such as `4` or `1.5`. Every CPU if left out. |
467−| `--pids-limit` | The most processes in a job container. 4096 if left out. |
468−| `--no-job-docker` | Give jobs no [Docker of their own](#docker-in-jobs). |
469−| `--job-docker-image` | The image a job's own Docker Engine runs. `docker:28-dind` if left out. |
470−| `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. |
471−| `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. |
472−| `--replace` | Take the place of a runner with the same name. |
473−| `--no-auto-update` | Never update itself. |
474−
475−To run several runners on one machine, give each its own `--dir` and
476−`--name`.
477−
478−## Use it in a workflow
479−
480−A job runs on a self-hosted runner when its `runs-on` names `self-hosted`:
481−
482−```yaml
483−jobs:
484− test:
485− runs-on: self-hosted
486− steps:
487− - uses: actions/checkout@v5
488− - run: make test
489−
490− gpu:
491− runs-on: [self-hosted, linux, gpu]
492− steps:
493− - run: nvidia-smi
494−
495− windows:
496− runs-on: [self-hosted, windows]
497− steps:
498− - run: Get-ComputerInfo | Select-Object OsName
499−```
500−
501−Until a runner with every label the job names takes it, the job waits, and
502−its page says what for: *Waiting for a self-hosted runner with labels
503−self-hosted, linux, gpu.* A job that waits a day fails. Jobs that have
504−waited ten minutes with no matching runner online show under **Needs you**
505−on Mission control.
506−
507−`runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group
508−called GPU as well.
509−
510−### Labels
511−
512−Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and
513−its architecture (`x64` or `arm64`), then the labels you gave it. A runner
514−that runs jobs in Docker is `linux`, whatever the machine is, since that is
515−what its jobs run on. Labels are matched without regard to case.
516−
517−A job takes a runner only when every label in its `runs-on` is one of the
518−runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or
519−`g1t-4core`, never go to a self-hosted runner.
520−
521−### What a job gets
522−
523−A job on your runner runs exactly as it would in g1t's sandbox: the same
524−harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and
525−workflow commands, the same log and artifacts. `runner.name`, `runner.os`
526−and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is
527−`self-hosted`.
528−
529−- **In a container** (the default), each job gets a fresh container from
530− its `container:` image or the runner's `--image`, removed when it ends,
531− with the walls in [what a job's container gets](#what-a-jobs-container-gets).
532− In the runner's image under Sysbox, its `services:` and Docker actions
533− get [a Docker Engine of its own](#docker-in-jobs).
534−- **With `--no-docker`** (deprecated), each job gets a fresh folder under
535− the work folder, removed when it ends, and runs with whatever the
536− machine has installed. A step's default shell is `bash` on Linux and
537− macOS and PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`,
538− `bash` and `python` work where installed. On a machine with Docker, the
539− job's `services:`, `container:`, `docker://` steps and Docker actions
540− use the machine's Docker, as on GitHub's runners.
541−
542−A self-hosted job stops at 360 minutes unless its `timeout-minutes` says
543−otherwise, as on GitHub, and can ask for up to 24 hours (1440). Jobs on
544−g1t's own machines stop at 60 minutes.
545−
546−## Groups
547−
548−A workspace's runners are in groups, which say which repositories may use
549−them. Every workspace has a **Default** group, for every repository, which
550−runners join unless their token or `--group` names another.
551−
552−To keep a set of machines for some repositories, open the workspace's
553−**Runners** page, click **New group**, name it, and choose **Only these**
554−repositories. Then click **New runner** with that group chosen, or register
555−with `--group`. Deleting a group moves its runners to the default group.
556−
557−## Agents on your runners
558−
559−**Runners → Where work runs** can send g1t's own work to your
560−runners too: agent runs, checks, reviews, merge checks and the merge queue.
561−Switch on **Run g1t's work on self-hosted runners** and give the labels
562−a runner needs to take it (`self-hosted` is always one).
563−
564−- The agent works exactly as in g1t's sandbox: the same harness, with a
565− short-lived credential that can do only what that kind of run may, in
566− that repository, revoked when it ends.
567−- Its model calls still go through g1t's model proxy, `models.g1t.sh`, with
568− that credential, so budgets, caps and the audit log work as before. With
569− your own [model provider](/guides/models/), g1t charges nothing for the
570− run; otherwise the model is paid as usual and the machine is free.
571−- Agent work needs an image with git, Node, `tar` and the agent's CLI:
572− register the runner with `--agent-image` and an image of yours that has
573− them. Host mode on a Linux machine set aside for it, where `/work` can be
574− written, also works, and is deprecated. g1t does not publish an agent
575− image yet.
576−- [Guardrails](/guides/guardrails/) still apply to what the agent does, but
577− their network list cannot be enforced on your machine. The run's session
578− says so when it starts.
579−
580−A project can have its own setting, or follow the workspace's.
581−
582−## What a run gets from Customize
583−
584−When g1t's work runs Claude Code on a runner (on g1t's own machines too),
585−the job gets what the workspace set up in [Customize](/guides/customize/),
586−as the job's agent would have it. Before Claude Code starts, the runner
587−reads the workspace's bundle with the job's short-lived agent credential,
588−once per job, and makes it ready:
589−
590−| In the bundle | What the runner does | In Claude Code |
591−| --- | --- | --- |
592−| 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 |
593−| 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>` |
594−| 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>` |
595−
596−The run's session gets one line saying what was loaded: *Loaded 2 plugins,
597−1 library skill and 3 connectors from the workspace's Customize.* A plugin
598−that could not be fetched, or a bundle that could not be read, is named
599−there and the run goes on without it; nothing from Customize fails a run.
600−Plugins from GitHub need `tar` on the machine.
601−
602−A run on a fork's head loads none of the fork's own settings, hooks, MCP
603−servers or commands ([guardrails](/guides/guardrails/)). The workspace's
604−plugins and connectors are the workspace's, not the checkout's, so they are
605−still passed and merged; but every skill and command is switched off on
606−such a run, the plugins' and the library's included, so an agent cannot
607−invoke them there.
608−
609−The bundle carries the connectors' sign-ins, so only a job's agent
610−credential or a workspace's token reads it, never a person's:
611−[`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/),
612−scope `agents:run`. g1t never keeps or logs it, and the runner keeps the
613−keys it brought out of the session. See
614−[what a run gets](/guides/customize/#what-a-run-gets).
615−
616−## Billing
617−
618−Time on your runners is free. Each job's minutes go on
619−[usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0,
620−so you can see how much ran there.
621−
622−- Workflow jobs on your runners need no plan and no card: they work on the
623− free tier.
624−- Agent work on your runners still needs its model paid for: the g1t plan,
625− the trial, the open-source pool on a public repository, or your own
626− model provider, which makes the run free.
627−
628−## Security
629−
630−- **Pull requests from forks never run on your runners** unless you allow
631− it under **Runners → Where work runs**. Leave it off on a
632− public repository: anyone who can open a pull request could run any code
633− on the machine, read what it can reach, and leave something behind for
634− the next job. Such jobs get no secrets either way.
635−- **Secrets** reach a job on your runner under the same rules as on g1t's:
636− only trusted runs get them, and only the ones for the job's environment.
637−- **The runner's credential** can poll for work, report on what it was
638− given and remove itself, and nothing else; every other endpoint refuses
639− it. It is kept only as a hash on g1t's side, rotates every day, and stops
640− working the moment the runner is removed. It is never given to a job:
641− each job gets only its own short-lived token.
642−- **Isolation**: run the runner's image under Sysbox. Each job then gets
643− a container of its own inside the runner's container, removed after,
644− and neither reaches the machine. A job with `--no-docker` runs as the
645− runner's user and can reach whatever that user can; host mode is
646− deprecated and refuses public repositories' work. Use ephemeral runners
647− for code you do not trust. See [isolation](#isolation).
648−- **Network**: g1t's network guardrails are not enforced on your machines.
649− A job reaches whatever the machine can.
650−- **Audit**: making a registration token, registering, removing, and every
651− job a runner takes are in the [audit log](/guides/audit-log/), with the
652− runner as the actor.
653−- Only owners (or a repository's admins) register and remove runners,
654− signed in or with a person's token with `runners:admin`. Workspace
655− tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine
656− to run its own jobs.
657−
658−## Docker and Kubernetes
659−
660−[Run it as a container](#run-it-as-a-container) covers Docker. The image's
661−settings, besides the register options:
662−
663−| Variable | |
664−| --- | --- |
665−| `G1T_RUNNER_SANDBOX` | `nested` in the image: start a Docker Engine inside the container and run jobs on it. |
666−| `G1T_RUNNER_DIR` | `/data` in the image: where the configuration and credential are kept. |
667−| `G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS` | `1` starts the runner even when it can reach the machine. See [above](#run-it-as-a-container). |
668−
669−A runner that ran the image with the machine's Docker socket, as these
670−docs said before, refuses to start with the new image. Remove the
671−`-v /var/run/docker.sock:/var/run/docker.sock` and add
672−`--runtime=sysbox-runc`; the `/data` volume keeps its registration.
673−
674−### Autoscaling
675−
676−Ephemeral runners take one job and remove themselves, so each job starts in
677−a fresh runner container. In Kubernetes with Sysbox installed on the
678−nodes (its Kubernetes install adds the `sysbox-runc` runtime class), run
679−them in pods that make their own registration token from an owner's
680−personal [access token](/guides/authentication/#access-tokens) with only
681−`runners:admin`, and let the Deployment replace each pod that finishes:
682−
683−```yaml
684−apiVersion: apps/v1
685−kind: Deployment
686−metadata:
687− name: g1t-runner
688−spec:
689− replicas: 3
690− selector: { matchLabels: { app: g1t-runner } }
691− template:
692− metadata:
693− labels: { app: g1t-runner }
694− annotations: { io.kubernetes.cri-o.userns-mode: "auto:size=65536" }
695− spec:
696− runtimeClassName: sysbox-runc
697− containers:
698− - name: runner
699− image: g1t.sh/flagon-io/g1t-runner
700− command: ["sh", "-c"]
701− args:
702− - |
703− export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \
704− -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \
705− https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \
706− | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
707− exec g1t-runner register-and-run
708− env:
709− - { name: G1T_RUNNER_URL, value: https://g1t.sh }
710− - { name: G1T_RUNNER_LABELS, value: k8s }
711− - { name: G1T_RUNNER_EPHEMERAL, value: "1" }
712− - name: G1T_ADMIN_TOKEN
713− valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } }
714−```
715−
716−To scale with demand rather than keep a fixed number, change `replicas`
717−with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/)
718−says which are busy.
719−
720−## Updates
721−
722−The runner checks for a new release every six hours while it is idle, and
723−updates itself when the release is signed by g1t's release key and its
724−download matches the release's SHA-256. `g1t-runner update` does it now;
725−`--no-auto-update` turns it off. Releases are at
726−`https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file.
727−The runner inside [the sandbox](#sandbox-on-your-computer) does not update
728−itself: `g1t-runner sandbox update` swaps the whole sandbox's image, which
729−carries it, once the running job is done.
730−
731−## Remove a runner
732−
733−On the machine, `g1t-runner remove` unregisters it and forgets its
734−credential (run `service uninstall` first if it is a service). Or remove it
735−from the **Runners** page; the runner stops on its next poll. A job it was
736−running fails. Runners offline for 14 days are removed by themselves.
737−
738−## API and MCP
739−
740−| What | REST | MCP (`workflow` tool) | Scope |
741−| --- | --- | --- | --- |
742−| 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` |
743−| Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` |
744−| Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
745−| 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` |
746−| 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` |
747−| 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 |
748−
749−The [Agent preset](/guides/authentication/#scopes) does not include
750−`runners:read`.
751−
752−## Troubleshooting
753−
754−| What you see | What to do |
755−| --- | --- |
756−| 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. |
757−| *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. |
758−| `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. |
759−| `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. |
760−| `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. |
761−| *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). |
762−| *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). |
763−| 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`. |
764−| *… 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. |
765−| `sandbox up` says *WSL is not installed* | Open a terminal as administrator, run `wsl --install --no-distribution`, restart, and run `sandbox up` again. That is the one step that needs an administrator. |
766−| `sandbox up` says *WSL is set to mirrored networking* | Remove `networkingMode=mirrored` from `%USERPROFILE%\.wslconfig`, run `wsl --shutdown`, and try again. |
767−| `sandbox status` says *this sandbox can reach the computer* | Something of your computer's is mounted in the sandbox's VM, or an engine's socket is there; it lists what. Run `g1t-runner sandbox down --delete` and `up` again, which makes it from the image afresh. |
768−| `sandbox status` shows the Linux runner in `error` with *The runner stopped …* | The runner inside could not register or start; the message is its last line. Check the token was still valid (an hour), and that the computer reaches `https://api.g1t.sh`. |
769−| The `windows` capability is unavailable | `status` says why: Windows Sandbox is off, this is Windows 11 Home, or Windows is older than 24H2. |
770−| 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`. |
771−| *could not run docker* | Install Docker and start it, and give the runner's user access to it. |
772−| Agent work fails at once | Give the runner `--agent-image`. |
773−| 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`. |
+796−0
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+import { TabItem, Tabs } from '@astrojs/starlight/components';
7+
8+A self-hosted runner is a machine of yours that runs your workflow jobs
9+instead of g1t's sandboxes. It can be a server, a Mac in a cupboard, a
10+Windows box with a GPU, or a pod in your cluster. On Linux you run
11+`g1t-runner` as a container under [Sysbox](#run-it-as-a-container), so
12+neither the runner nor its jobs can reach the machine; it registers once
13+and picks up jobs that ask for it with `runs-on: self-hosted`.
14+
15+- **Time on your runners is $0**, on every plan, including the free one.
16+ It shows on [usage](/guides/usage-and-billing/) as self-hosted minutes.
17+- **It only connects out.** The runner polls `api.g1t.sh` over HTTPS for
18+ work. Nothing needs to reach the machine: no open port, no tunnel.
19+- **Each job in a fresh sandbox.** Every job gets a container of its own,
20+ made for it and removed after it, inside the runner's own container.
21+ See [isolation](#isolation).
22+- **Any OS.** Linux, macOS and Windows, on x64 and arm64. On your own
23+ computer, `g1t-runner sandbox up` runs Linux jobs in a Linux VM of the
24+ runner's own and Windows jobs each in a Windows Sandbox: see
25+ [sandbox on your computer](#sandbox-on-your-computer).
26+
27+| Where | Page | Who manages it |
28+| --- | --- | --- |
29+| A workspace | **Workspace → Runners** in the rail, `g1t.sh/<workspace>/-/runners` | Owners only. |
30+| A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository |
31+
32+A workspace's runners serve the repositories their [group](#groups) allows.
33+A project's own runners serve only that project.
34+
35+## Run it as a container
36+
37+On a Linux machine, run the runner's image with the Sysbox runtime. The
38+runner starts a Docker Engine of its own inside its container and runs
39+each job in a container on that engine, so jobs are containers inside the
40+runner's container. Sysbox gives the runner's container a user namespace
41+of its own, so its root is an unprivileged user on the machine, and lets
42+it run Docker without `--privileged` and without the machine's Docker
43+socket.
44+
45+1. Install [Sysbox](https://github.com/nestybox/sysbox) 0.7 or later on
46+ the machine, following its install guide. Earlier versions cannot run
47+ the runner's own Docker Engine, whose `runc` checks `/proc` in a way
48+ they do not allow. Sysbox needs Linux; on a Mac or Windows computer,
49+ use [the sandbox](#sandbox-on-your-computer) instead.
50+2. Make a registration token: open **Runners** and click **New runner**.
51+3. Run the runner:
52+
53+ ```sh
54+ docker run -d --name g1t-runner --restart unless-stopped \
55+ --runtime=sysbox-runc \
56+ -v g1t-runner:/data \
57+ -e G1T_RUNNER_URL=https://g1t.sh \
58+ -e G1T_RUNNER_TOKEN=g1trt_… \
59+ g1t.sh/flagon-io/g1t-runner
60+ ```
61+
62+The image runs `register-and-run`, which registers once and then runs.
63+Its configuration and credential are kept on the `g1t-runner` volume, so
64+the container can be restarted and replaced without registering again.
65+Every [register option](#register-options) can come from
66+`G1T_RUNNER_<OPTION>` in the environment, such as `G1T_RUNNER_LABELS=gpu`
67+or `G1T_RUNNER_MEMORY=8g`, so a secret can hold the token.
68+
69+Its log says which isolation it has:
70+
71+```text
72+… 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.
73+```
74+
75+Do not give the container the machine's Docker socket
76+(`-v /var/run/docker.sock:/var/run/docker.sock`) or `--privileged`. The
77+runner looks for both when it starts, and refuses to start if it finds
78+them:
79+
80+| It finds | What it says |
81+| --- | --- |
82+| 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* |
83+| 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* |
84+| `DOCKER_HOST` set | *DOCKER_HOST is set …* |
85+| Root in the container is root on the machine (no Sysbox, or `--privileged`) | *root in this container is root on the machine* |
86+
87+To start anyway, with jobs able to reach the machine, set
88+`G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1`. The runner then logs each
89+problem with `UNSAFE:` in front, and registers as `container` rather than
90+`nested-container`.
91+
92+The engine's images and build cache are kept in the container, so they
93+last until the container is removed. Sysbox keeps them on the machine's
94+disk for that container alone; to keep them when you replace the
95+container, add `-v g1t-runner-docker:/var/lib/docker`, a volume for one
96+runner container at a time.
97+
98+## Isolation
99+
100+How well a runner keeps work from its machine. A runner tells g1t its
101+level when it registers and on every poll, and the **Runners** page shows
102+it on each runner: *Sandboxed*, *VM per job*, *Shared Docker* or
103+*Unsandboxed*.
104+
105+| Isolation | How you get it | What a job can reach |
106+| --- | --- | --- |
107+| `nested-container` | The runner's image under Sysbox, as above; on your own computer, `g1t-runner sandbox up`, which runs the same inside a Linux VM of the runner's own; 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. |
108+| `vm` | `g1t-runner sandbox up` on Windows 11 Pro, Enterprise or Education, or `register --native` there: jobs native to Windows, each in a [Windows Sandbox](#windows-jobs-in-windows-sandbox). | Its own virtual machine, made for the job and discarded after, with a read-only copy of the harness and no other folder of the machine's. |
109+| `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. |
110+| `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. |
111+
112+### What a job's container gets
113+
114+With `nested-container` and `container` isolation, each job runs in a
115+container made for it and removed after it:
116+
117+| | |
118+| --- | --- |
119+| Image | The job's `container:`, else the runner's `--image`, else `node:24-bookworm`. |
120+| 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. |
121+| Network | A Docker network of its own, removed after the job. Never the machine's network. |
122+| 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. |
123+| 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. |
124+| 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). |
125+| Credentials | Only the job's own short-lived token, passed by name. Never the runner's credential. |
126+
127+### Docker in jobs
128+
129+With `nested-container` isolation, a job that uses Docker (its
130+`services:`, a Docker action, a `docker://` step, or `docker` in a step)
131+gets a Docker Engine of its own. It starts the first time the job uses
132+Docker, from `docker:28-dind`, beside the job's container and on the job's
133+network, so a service's published ports are on the job's `localhost`. It
134+is removed, with its images and containers, when the job ends. A job that
135+does not use Docker never starts one.
136+
137+The job's engine is privileged inside the runner's container. Sysbox keeps
138+it off the machine, but a job that uses Docker can reach the runner's own
139+container, the runner's credential included. For code you do not trust,
140+use [ephemeral runners](#autoscaling), so each job has a runner container
141+of its own, or register with `--no-job-docker`, which gives jobs no Docker.
142+
143+With `container` isolation, jobs have no Docker of their own, since it
144+would be the machine's: their `services:` are not started, and the log
145+says so.
146+
147+### Host mode is deprecated
148+
149+`--no-docker` still works, and runs each job directly on the machine in a
150+folder of its own. It will be removed. Until then:
151+
152+- The runner's log says so when it starts: *Deprecated: this runner runs
153+ jobs directly on this machine (--no-docker), unsandboxed.*
154+- Every job's log starts with a warning: *This job ran on the host,
155+ unsandboxed …*
156+- It is not given a public repository's work, which anyone can propose
157+ code to. The runner says how it is isolated when it registers and on
158+ every poll, and g1t hands a public repository's jobs and agent work
159+ only to runners that are not in host mode. A job no other online runner
160+ could take waits, and its page says *Waiting for a sandboxed runner:
161+ this repository is public, and host-mode runners don't run public
162+ repositories' jobs.* It starts once a sandboxed runner with its labels
163+ comes online.
164+- A runner from before this, which does not say how it is isolated, may
165+ still be given such a job and refuses it itself: *acme/web is public,
166+ and this self-hosted runner runs jobs directly on its machine
167+ (--no-docker), unsandboxed, so it does not take a public repository's
168+ work.* The job fails with that message. Update the runner.
169+
170+Move a Linux runner to [the container](#run-it-as-a-container), and a
171+Windows or Mac computer to [the sandbox](#sandbox-on-your-computer). A
172+runner registers as running jobs native to Windows or macOS (the
173+`windows` or `macos` label) only when it sandboxes them (`--native`), or
174+when you chose host mode with `--no-docker`.
175+
176+## Sandbox on your computer
177+
178+`g1t-runner sandbox` sets up self-hosted runners on your own desktop or
179+laptop that nothing a job runs can get out of: not to your files, your
180+other programs, your processes or your Docker. It gives the computer up
181+to two capabilities, each a runner of its own on the **Runners** page:
182+
183+| Capability | Runner | Labels | Isolation |
184+| --- | --- | --- | --- |
185+| Linux jobs | `<name>-linux`, inside a Linux VM of the runner's own | `self-hosted`, `linux`, and the architecture | `nested-container` |
186+| Jobs native to the computer's OS | `<name>-windows`, on the computer, starting a Windows Sandbox per job | `self-hosted`, `windows`, and the architecture | `vm` |
187+
188+1. Make a registration token: open **Runners** and click **New runner**.
189+2. Download `g1t-runner` for your computer, as in [add a runner](#add-a-runner),
190+ then run:
191+
192+ ```sh
193+ g1t-runner sandbox up --url https://g1t.sh --token g1trt_…
194+ ```
195+
196+The first `up` fetches the sandbox's image for this release, checks its
197+signature and SHA-256, makes the VM, and registers both runners with the
198+token; it takes a few minutes. After that, `up` starts what is stopped
199+and needs no token.
200+
201+| Command | What it does |
202+| --- | --- |
203+| `g1t-runner sandbox up` | Makes the sandbox if there is none, and starts both runners. Waits until the Linux runner is asking for work. |
204+| `g1t-runner sandbox down` | Stops both now. A running job is cut short, and its run says the runner was stopped. `--drain` waits for it to finish instead. `--delete` also removes the VM and everything in it; the runners stay listed on g1t until you remove them there. |
205+| `g1t-runner sandbox down --only windows` | Stops Windows jobs alone and keeps them off: Linux jobs carry on, and a later `up` leaves Windows jobs stopped. `--only linux` does the same for Linux jobs, leaving Windows jobs running. Takes `--drain`, not `--delete`. |
206+| `g1t-runner sandbox up --only windows` | Turns Windows jobs on again and starts them, leaving Linux jobs as they are. `--only linux,windows` turns on both. |
207+| `g1t-runner sandbox status` | Says how each capability is, and what to do about one that is not available. |
208+| `g1t-runner sandbox update` | Drains the Linux runner (the job it is running finishes), swaps in the newest release's image, and starts it again. The registration is kept, so it is the same runner on g1t. |
209+
210+| Option (`up`) | |
211+| --- | --- |
212+| `--url`, `--token` | Where to register and the registration token, for the first `up`. The token can come from `G1T_RUNNER_TOKEN` in the environment instead, which keeps it out of the process list. |
213+| `--name` | The runners' name before `-linux` and `-windows`. The computer's name if left out. |
214+| `--labels`, `--group` | As for [register](#register-options), for both runners. |
215+| `--cpus` | The Linux VM's runner's CPUs, and so its jobs'. Half the computer's, at most 8, if left out. |
216+| `--memory` | Its memory, such as `8g`. A quarter of the computer's, between 2 and 8 GB and never more than half, if left out. |
217+| `--disk` | The VM's disk on macOS and Linux, such as `64g` (64 GB if left out). On Windows the disk is not limited: WSL can only resize it with all of WSL shut down. |
218+| `--only` | The kinds of job to start, of `linux`, `windows` and `macos` (`native` is this computer's own): turned on, and kept on. The others are left as they are, running or not. Without it, `up` starts every kind that isn't turned off. `down` takes it too. |
219+| `--no-native`, `--native` | Turn the native capability off, or on again, as `down --only` and `up --only` do. Kept until said otherwise. |
220+| `--state-dir` | Where the sandbox keeps its settings and images. `~/.g1t-runner/sandbox` if left out. Every command takes it. |
221+| `--json` | Print the status as JSON when done (see below). Every command takes it. |
222+
223+### How each computer runs it
224+
225+| Computer | Linux jobs | Native jobs |
226+| --- | --- | --- |
227+| Windows 11 Pro, Enterprise, Education | A WSL 2 distribution of the runner's own, `g1t-sandbox` | Windows Sandbox, one per job |
228+| Windows 11 Home | A WSL 2 distribution of the runner's own, `g1t-sandbox` | Not available: Home has no Windows Sandbox |
229+| macOS 13 or later | A VM under Apple's Virtualization framework, run by [vfkit](https://github.com/crc-org/vfkit) (`brew install vfkit`) | Not released: sandboxed macOS jobs are designed, not built |
230+| Linux with Sysbox 0.7+ | No VM: the runner's image under Sysbox, as [above](#run-it-as-a-container) | None (Linux jobs are native) |
231+| Linux without Sysbox | A VM under QEMU with KVM (`/dev/kvm`, and `qemu-system-x86_64` or `qemu-system-aarch64`) | None |
232+
233+Inside the VM (and on Linux with Sysbox) it is the runner's image under
234+Sysbox, exactly as in [run it as a container](#run-it-as-a-container):
235+each job is a fresh container on the runner's own Docker Engine, inside the
236+runner's Sysbox container, inside the VM. Nothing listens on your
237+computer; the VM only connects out.
238+
239+**What the VM gets of your computer:** no folders and no drives, no way
240+to start its programs, no Docker socket, no clipboard, no GPU, display or
241+sound, and no network port in. On Windows, the distribution's
242+`/etc/wsl.conf` turns off WSL's drive mounts, its interop with Windows
243+programs and Windows' `PATH`, and when it starts the sandbox also unmounts
244+what WSL puts in every distribution anyway: `/mnt/wsl` (shared by all
245+distributions, where Docker Desktop keeps sockets to its engine on
246+Windows), WSLg's display and sound sockets, and Windows' GPU driver
247+folders. It checks that nothing of the computer's is left, and refuses to
248+run jobs if anything is. Containers in the VM, every job among them, may
249+reach the internet but not your computer: a firewall rule in the VM drops
250+their traffic to the computer's address, but for DNS.
251+
252+**Resources:** the runner's container has `--cpus` and `--memory`, and
253+each job has its own limits inside that ([what a job's container gets](#what-a-jobs-container-gets)).
254+On Windows these are the sandbox's share of WSL's own VM, which
255+`%USERPROFILE%\.wslconfig` sizes for all of WSL.
256+
257+**Images and updates:** the sandbox's images are published with each
258+runner release, at `g1t.sh/downloads/runner/<version>/`, and named with
259+their SHA-256 in the release's manifest, which is signed with g1t's
260+release key like the runner itself (see [updates](#updates)). The runner's
261+image inside is named by digest in the same manifest. `update` checks the
262+signature and every SHA-256 before using anything, and waits for the
263+running job before it swaps the image. The runner's registration lives
264+apart from the image (on its own disk on macOS and Linux; on Windows the
265+launcher carries it across), so an update does not register again.
266+
267+### Windows jobs in Windows Sandbox
268+
269+With `vm` isolation, the runner on your computer takes jobs that ask for
270+`runs-on: [self-hosted, windows]` and runs each in a Windows Sandbox of
271+its own: a Hyper-V virtual machine with its own kernel, started fresh from
272+your Windows and discarded, disk and all, when the job ends.
273+
274+- Its only folder from your computer is read-only and holds a copy of the
275+ runner (the job's harness) and the job's script. Nothing it writes
276+ outlives it.
277+- No GPU, clipboard, printers, microphone or camera; networking on, so the
278+ job can reach g1t and the internet.
279+- A firewall rule in it blocks the address your computer is reached by.
280+ The job runs as an administrator in its sandbox and could remove it, so
281+ it slows a job down rather than stopping it; the wall is the VM.
282+- One job at a time, as every runner. Agent work runs on Linux runners.
283+- It needs Windows 11 Pro, Enterprise or Education, version 24H2 or later
284+ (for the `wsb` command), with **Windows Sandbox** turned on under
285+ Settings, System, Optional features, More Windows features. `status`
286+ says which of these is missing.
287+
288+On a runner that is not under `sandbox up`, `g1t-runner register --native`
289+does the same.
290+
291+### What it cannot stop
292+
293+- **WSL 2 distributions share one VM.** On Windows, every WSL 2
294+ distribution, your own among them, runs in one virtual machine with one
295+ Linux kernel, and root in one can reach the others' disks. That is why
296+ nothing a job runs is root in the sandbox's distribution: the runner is
297+ a Sysbox container whose root is an unprivileged user there, and each job
298+ a container inside it. A job would have to escape its container and the
299+ runner's, and then reach root in the distribution, before it could touch
300+ your other distributions, from which Windows is reachable.
301+- **Mirrored networking.** With `networkingMode=mirrored` in
302+ `.wslconfig`, Linux reaches Windows' own `localhost`, so the sandbox
303+ will not start until it is removed.
304+- **The computer's network.** Jobs reach the internet, and so the other
305+ machines on your network that the internet could. Only your computer's
306+ own address is blocked.
307+- **The VM's own root** could reach your computer's services on its
308+ address. Jobs are two containers away from it.
309+- **A Windows job's sandbox** is as strong as Windows Sandbox: a Hyper-V
310+ VM, with one read-only folder of your computer's.
311+- **macOS and Linux VMs** (vfkit and QEMU) are built to the same design,
312+ with no shared folders and nothing forwarded in, but have not yet been
313+ run on a Mac or a Linux machine without Sysbox. Windows is where the
314+ sandbox has been checked end to end.
315+
316+### The status as JSON
317+
318+`g1t-runner sandbox status --json` prints one JSON object, which [the
319+desktop app](/guides/desktop/) shows. It exits 0 whenever it could read
320+the status, a sandbox in error included; `up`, `down` and `update` print
321+the same object with `--json` when they are done, and exit 1 with
322+`error` set when they fail.
323+
324+```json
325+{
326+ "schema": 1,
327+ "state": "running",
328+ "backend": "wsl2",
329+ "runner_version": "0.3.0",
330+ "launcher_version": "0.3.0",
331+ "image_version": "0.3.0",
332+ "isolation": "nested-container",
333+ "job": { "kind": "workflow", "id": "job_…", "name": "build", "repo": "acme/web" },
334+ "error": null,
335+ "resources": { "cpus": 4, "memory_mib": 8192, "disk_gib": 64 },
336+ "capabilities": [
337+ { "id": "linux", "available": true, "state": "running", "isolation": "nested-container",
338+ "runner_version": "0.3.0", "job": null, "cause": null, "fix": null, "turned_off": false },
339+ { "id": "windows", "available": false, "state": "absent", "isolation": "vm",
340+ "runner_version": null, "job": null, "turned_off": false,
341+ "cause": "Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).",
342+ "fix": "On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox under Settings, System, Optional features, More Windows features, then restart." }
343+ ],
344+ "state_dir": "C:\\Users\\you\\.g1t-runner\\sandbox"
345+}
346+```
347+
348+| Field | |
349+| --- | --- |
350+| `state` | The Linux VM's: `absent` (never made, or deleted), `stopped`, `starting`, `running`, `updating` or `error`. |
351+| `backend` | What holds it: `wsl2`, `vfkit`, `qemu` or `sysbox`. |
352+| `runner_version`, `image_version`, `launcher_version` | The runner inside the VM, the VM's image, and this `g1t-runner`. |
353+| `job` | What the Linux runner is running now, or null. |
354+| `error` | A sentence for a person when something is wrong, or null. |
355+| `capabilities` | Each kind of job the computer can run sandboxed: `linux`, and `windows` or `macos`. `available` says whether it can be had here; when not, `cause` says why and `fix` what to do. Each has its own `state`, `job` and `runner_version`. A native one whose runner didn't start is in `error`, with why in `cause`. |
356+| `turned_off` | On each capability: someone stopped it (`down --only`, `--no-native`). Its `state` is then `stopped`: stopped by a person, not failing, and nothing in `error`. |
357+
358+Fields are only added; `schema` changes if one changes meaning.
359+
360+## The Runners page
361+
362+A workspace's **Runners** page shows g1t cloud and your own runners side
363+by side, with what each is running now and this month's time on each.
364+
365+| Part | What it shows |
366+| --- | --- |
367+| **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. |
368+| **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. |
369+| **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. |
370+| **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). |
371+| **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. |
372+| **Where work runs** | What decides where each kind of work runs, and the [agent setting](#agents-on-your-runners). |
373+
374+A runner is **Idle** when it is online with nothing to do, **Busy** while it
375+runs a job or an agent run, and **Offline** when it has not polled for 90
376+seconds. When a service does not answer, only its part of the page says so.
377+
378+Sessions that persist are here for workspace agents: each has
379+[a computer of its own](/guides/agents/#its-computer) on g1t cloud, whose
380+home stays between sessions and which sleeps when idle; the page says so
381+under **Agents' computers and more**. Pinning an agent's computer to one of
382+your own runners and an official agent image are coming, and the page
383+labels them so.
384+
385+**Desktops** lists every computer in the workspace with [the desktop
386+app](/guides/desktop/#the-runner), whose runner runs while g1t is open
387+there: what each is doing, or why it isn't running and how to fix it, in
388+the app's own words, and how it runs jobs. A computer whose g1t has been
389+closed for ten minutes says *g1t is closed on* its name, and so does its
390+runner in the list, rather than *Offline*.
391+
392+## A runner in the desktop app
393+
394+[The desktop app](/guides/desktop/#the-runner) runs a runner for you
395+while it is open, with nothing to install or register: it registers your
396+computer with the workspace you choose, as you, through the site, and
397+keeps the runner up to date. It runs jobs sandboxed by default
398+(`nested-container`); running them in Docker (`container`) or directly
399+on the computer (`host`) are settings you choose, and the app never falls
400+back to them. Sandboxed, it is [the sandbox](#sandbox-on-your-computer),
401+which the app sets up with `g1t-runner sandbox up` and shows with
402+`sandbox status --json`: Linux jobs and, on Windows 11 Pro and up,
403+Windows jobs, each a runner of its own. Its runners have the label
404+`desktop` besides the usual ones.
405+
406+## Add a runner
407+
408+On Linux, [run it as a container](#run-it-as-a-container). To install
409+`g1t-runner` on a machine instead, as on macOS and Windows:
410+
411+1. Open **Runners** (a project's: **Settings → Runners**) and click **New runner**. g1t makes a
412+ registration token and shows the commands for Linux, macOS, Windows and
413+ Docker with it filled in. The token lasts an hour, can register any
414+ number of runners until then, and is shown once.
415+2. On the machine, download `g1t-runner` and register it:
416+
417+ <Tabs syncKey="os">
418+ <TabItem label="macOS">
419+ ```sh
420+ # Apple silicon; use g1t-runner-macos-x64 on Intel
421+ curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64
422+ chmod +x g1t-runner
423+ ./g1t-runner register --url https://g1t.sh --token g1trt_…
424+ ```
425+ </TabItem>
426+ <TabItem label="Linux">
427+ ```sh
428+ # x64; use g1t-runner-linux-arm64 on arm64
429+ curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64
430+ chmod +x g1t-runner
431+ ./g1t-runner register --url https://g1t.sh --token g1trt_…
432+ ```
433+ </TabItem>
434+ <TabItem label="Windows">
435+ ```powershell
436+ Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe
437+ .\g1t-runner.exe register --url https://g1t.sh --token g1trt_…
438+ ```
439+ </TabItem>
440+ </Tabs>
441+
442+3. Start it: `./g1t-runner run` keeps it running in the terminal, or
443+ install it as a service that starts with the machine:
444+
445+ <Tabs syncKey="os">
446+ <TabItem label="macOS">
447+ ```sh
448+ ./g1t-runner service install
449+ ```
450+ A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log`.
451+ </TabItem>
452+ <TabItem label="Linux">
453+ ```sh
454+ sudo ./g1t-runner service install
455+ ```
456+ A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>`.
457+ </TabItem>
458+ <TabItem label="Windows">
459+ From an Administrator prompt:
460+ ```powershell
461+ .\g1t-runner.exe service install
462+ ```
463+ A Windows service, `g1t-runner-<name>`, that restarts if it stops.
464+ </TabItem>
465+ </Tabs>
466+
467+ `service start`, `stop`, `status` and `uninstall` do what they say.
468+
469+The runner shows up in the list within a few seconds, with a green dot.
470+
471+Installed this way, it has `container` isolation with Docker, or `host`
472+isolation with `--no-docker`. See [isolation](#isolation).
473+
474+### Register options
475+
476+| Option | |
477+| --- | --- |
478+| `--url` | The g1t you register with: `https://g1t.sh`. |
479+| `--token` | The registration token. |
480+| `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). |
481+| `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). |
482+| `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. |
483+| `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). |
484+| `--no-docker` | Deprecated. Run jobs directly on the machine instead of in containers ([host mode](#host-mode-is-deprecated)). |
485+| `--native` | Run jobs native to this OS, each in a throwaway VM of its own: [Windows Sandbox](#windows-jobs-in-windows-sandbox) on Windows. Registers with the OS's label and `vm` isolation, and refuses if the sandbox is not there. |
486+| `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. |
487+| `--agent-image` | The image [agent work](#agents-on-your-runners) runs in. |
488+| `--memory` | Each job container's memory, such as `8g`. Three quarters of the runner's memory if left out. |
489+| `--cpus` | Each job container's CPUs, such as `4` or `1.5`. Every CPU if left out. |
490+| `--pids-limit` | The most processes in a job container. 4096 if left out. |
491+| `--no-job-docker` | Give jobs no [Docker of their own](#docker-in-jobs). |
492+| `--job-docker-image` | The image a job's own Docker Engine runs. `docker:28-dind` if left out. |
493+| `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. |
494+| `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. |
495+| `--replace` | Take the place of a runner with the same name. |
496+| `--no-auto-update` | Never update itself. |
497+
498+To run several runners on one machine, give each its own `--dir` and
499+`--name`.
500+
501+## Use it in a workflow
502+
503+A job runs on a self-hosted runner when its `runs-on` names `self-hosted`:
504+
505+```yaml
506+jobs:
507+ test:
508+ runs-on: self-hosted
509+ steps:
510+ - uses: actions/checkout@v5
511+ - run: make test
512+
513+ gpu:
514+ runs-on: [self-hosted, linux, gpu]
515+ steps:
516+ - run: nvidia-smi
517+
518+ windows:
519+ runs-on: [self-hosted, windows]
520+ steps:
521+ - run: Get-ComputerInfo | Select-Object OsName
522+```
523+
524+Until a runner with every label the job names takes it, the job waits, and
525+its page says what for: *Waiting for a self-hosted runner with labels
526+self-hosted, linux, gpu.* A job that waits a day fails. Jobs that have
527+waited ten minutes with no matching runner online show under **Needs you**
528+on Mission control.
529+
530+`runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group
531+called GPU as well.
532+
533+### Labels
534+
535+Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and
536+its architecture (`x64` or `arm64`), then the labels you gave it. A runner
537+that runs jobs in Docker is `linux`, whatever the machine is, since that is
538+what its jobs run on. Labels are matched without regard to case.
539+
540+A job takes a runner only when every label in its `runs-on` is one of the
541+runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or
542+`g1t-4core`, never go to a self-hosted runner.
543+
544+### What a job gets
545+
546+A job on your runner runs exactly as it would in g1t's sandbox: the same
547+harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and
548+workflow commands, the same log and artifacts. `runner.name`, `runner.os`
549+and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is
550+`self-hosted`.
551+
552+- **In a container** (the default), each job gets a fresh container from
553+ its `container:` image or the runner's `--image`, removed when it ends,
554+ with the walls in [what a job's container gets](#what-a-jobs-container-gets).
555+ In the runner's image under Sysbox, its `services:` and Docker actions
556+ get [a Docker Engine of its own](#docker-in-jobs).
557+- **With `--no-docker`** (deprecated), each job gets a fresh folder under
558+ the work folder, removed when it ends, and runs with whatever the
559+ machine has installed. A step's default shell is `bash` on Linux and
560+ macOS and PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`,
561+ `bash` and `python` work where installed. On a machine with Docker, the
562+ job's `services:`, `container:`, `docker://` steps and Docker actions
563+ use the machine's Docker, as on GitHub's runners.
564+
565+A self-hosted job stops at 360 minutes unless its `timeout-minutes` says
566+otherwise, as on GitHub, and can ask for up to 24 hours (1440). Jobs on
567+g1t's own machines stop at 60 minutes.
568+
569+## Groups
570+
571+A workspace's runners are in groups, which say which repositories may use
572+them. Every workspace has a **Default** group, for every repository, which
573+runners join unless their token or `--group` names another.
574+
575+To keep a set of machines for some repositories, open the workspace's
576+**Runners** page, click **New group**, name it, and choose **Only these**
577+repositories. Then click **New runner** with that group chosen, or register
578+with `--group`. Deleting a group moves its runners to the default group.
579+
580+## Agents on your runners
581+
582+**Runners → Where work runs** can send g1t's own work to your
583+runners too: agent runs, checks, reviews, merge checks and the merge queue.
584+Switch on **Run g1t's work on self-hosted runners** and give the labels
585+a runner needs to take it (`self-hosted` is always one).
586+
587+- The agent works exactly as in g1t's sandbox: the same harness, with a
588+ short-lived credential that can do only what that kind of run may, in
589+ that repository, revoked when it ends.
590+- Its model calls still go through g1t's model proxy, `models.g1t.sh`, with
591+ that credential, so budgets, caps and the audit log work as before. With
592+ your own [model provider](/guides/models/), g1t charges nothing for the
593+ run; otherwise the model is paid as usual and the machine is free.
594+- Agent work needs an image with git, Node, `tar` and the agent's CLI:
595+ register the runner with `--agent-image` and an image of yours that has
596+ them. Host mode on a Linux machine set aside for it, where `/work` can be
597+ written, also works, and is deprecated. g1t does not publish an agent
598+ image yet.
599+- [Guardrails](/guides/guardrails/) still apply to what the agent does, but
600+ their network list cannot be enforced on your machine. The run's session
601+ says so when it starts.
602+
603+A project can have its own setting, or follow the workspace's.
604+
605+## What a run gets from Customize
606+
607+When g1t's work runs Claude Code on a runner (on g1t's own machines too),
608+the job gets what the workspace set up in [Customize](/guides/customize/),
609+as the job's agent would have it. Before Claude Code starts, the runner
610+reads the workspace's bundle with the job's short-lived agent credential,
611+once per job, and makes it ready:
612+
613+| In the bundle | What the runner does | In Claude Code |
614+| --- | --- | --- |
615+| 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 |
616+| 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>` |
617+| 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>` |
618+
619+The run's session gets one line saying what was loaded: *Loaded 2 plugins,
620+1 library skill and 3 connectors from the workspace's Customize.* A plugin
621+that could not be fetched, or a bundle that could not be read, is named
622+there and the run goes on without it; nothing from Customize fails a run.
623+Plugins from GitHub need `tar` on the machine.
624+
625+A run on a fork's head loads none of the fork's own settings, hooks, MCP
626+servers or commands ([guardrails](/guides/guardrails/)). The workspace's
627+plugins and connectors are the workspace's, not the checkout's, so they are
628+still passed and merged; but every skill and command is switched off on
629+such a run, the plugins' and the library's included, so an agent cannot
630+invoke them there.
631+
632+The bundle carries the connectors' sign-ins, so only a job's agent
633+credential or a workspace's token reads it, never a person's:
634+[`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/),
635+scope `agents:run`. g1t never keeps or logs it, and the runner keeps the
636+keys it brought out of the session. See
637+[what a run gets](/guides/customize/#what-a-run-gets).
638+
639+## Billing
640+
641+Time on your runners is free. Each job's minutes go on
642+[usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0,
643+so you can see how much ran there.
644+
645+- Workflow jobs on your runners need no plan and no card: they work on the
646+ free tier.
647+- Agent work on your runners still needs its model paid for: the g1t plan,
648+ the trial, the open-source pool on a public repository, or your own
649+ model provider, which makes the run free.
650+
651+## Security
652+
653+- **Pull requests from forks never run on your runners** unless you allow
654+ it under **Runners → Where work runs**. Leave it off on a
655+ public repository: anyone who can open a pull request could run any code
656+ on the machine, read what it can reach, and leave something behind for
657+ the next job. Such jobs get no secrets either way.
658+- **Secrets** reach a job on your runner under the same rules as on g1t's:
659+ only trusted runs get them, and only the ones for the job's environment.
660+- **The runner's credential** can poll for work, report on what it was
661+ given and remove itself, and nothing else; every other endpoint refuses
662+ it. It is kept only as a hash on g1t's side, rotates every day, and stops
663+ working the moment the runner is removed. It is never given to a job:
664+ each job gets only its own short-lived token.
665+- **Isolation**: run the runner's image under Sysbox. Each job then gets
666+ a container of its own inside the runner's container, removed after,
667+ and neither reaches the machine. A job with `--no-docker` runs as the
668+ runner's user and can reach whatever that user can; host mode is
669+ deprecated and refuses public repositories' work. Use ephemeral runners
670+ for code you do not trust. See [isolation](#isolation).
671+- **Network**: g1t's network guardrails are not enforced on your machines.
672+ A job reaches whatever the machine can.
673+- **Audit**: making a registration token, registering, removing, and every
674+ job a runner takes are in the [audit log](/guides/audit-log/), with the
675+ runner as the actor.
676+- Only owners (or a repository's admins) register and remove runners,
677+ signed in or with a person's token with `runners:admin`. Workspace
678+ tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine
679+ to run its own jobs.
680+
681+## Docker and Kubernetes
682+
683+[Run it as a container](#run-it-as-a-container) covers Docker. The image's
684+settings, besides the register options:
685+
686+| Variable | |
687+| --- | --- |
688+| `G1T_RUNNER_SANDBOX` | `nested` in the image: start a Docker Engine inside the container and run jobs on it. |
689+| `G1T_RUNNER_DIR` | `/data` in the image: where the configuration and credential are kept. |
690+| `G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS` | `1` starts the runner even when it can reach the machine. See [above](#run-it-as-a-container). |
691+
692+A runner that ran the image with the machine's Docker socket, as these
693+docs said before, refuses to start with the new image. Remove the
694+`-v /var/run/docker.sock:/var/run/docker.sock` and add
695+`--runtime=sysbox-runc`; the `/data` volume keeps its registration.
696+
697+### Autoscaling
698+
699+Ephemeral runners take one job and remove themselves, so each job starts in
700+a fresh runner container. In Kubernetes with Sysbox installed on the
701+nodes (its Kubernetes install adds the `sysbox-runc` runtime class), run
702+them in pods that make their own registration token from an owner's
703+personal [access token](/guides/authentication/#access-tokens) with only
704+`runners:admin`, and let the Deployment replace each pod that finishes:
705+
706+```yaml
707+apiVersion: apps/v1
708+kind: Deployment
709+metadata:
710+ name: g1t-runner
711+spec:
712+ replicas: 3
713+ selector: { matchLabels: { app: g1t-runner } }
714+ template:
715+ metadata:
716+ labels: { app: g1t-runner }
717+ annotations: { io.kubernetes.cri-o.userns-mode: "auto:size=65536" }
718+ spec:
719+ runtimeClassName: sysbox-runc
720+ containers:
721+ - name: runner
722+ image: g1t.sh/flagon-io/g1t-runner
723+ command: ["sh", "-c"]
724+ args:
725+ - |
726+ export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \
727+ -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \
728+ https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \
729+ | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
730+ exec g1t-runner register-and-run
731+ env:
732+ - { name: G1T_RUNNER_URL, value: https://g1t.sh }
733+ - { name: G1T_RUNNER_LABELS, value: k8s }
734+ - { name: G1T_RUNNER_EPHEMERAL, value: "1" }
735+ - name: G1T_ADMIN_TOKEN
736+ valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } }
737+```
738+
739+To scale with demand rather than keep a fixed number, change `replicas`
740+with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/)
741+says which are busy.
742+
743+## Updates
744+
745+The runner checks for a new release every six hours while it is idle, and
746+updates itself when the release is signed by g1t's release key and its
747+download matches the release's SHA-256. `g1t-runner update` does it now;
748+`--no-auto-update` turns it off. Releases are at
749+`https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file.
750+The runner inside [the sandbox](#sandbox-on-your-computer) does not update
751+itself: `g1t-runner sandbox update` swaps the whole sandbox's image, which
752+carries it, once the running job is done.
753+
754+## Remove a runner
755+
756+On the machine, `g1t-runner remove` unregisters it and forgets its
757+credential (run `service uninstall` first if it is a service). Or remove it
758+from the **Runners** page; the runner stops on its next poll. A job it was
759+running fails. Runners offline for 14 days are removed by themselves.
760+
761+## API and MCP
762+
763+| What | REST | MCP (`workflow` tool) | Scope |
764+| --- | --- | --- | --- |
765+| 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` |
766+| Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` |
767+| Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
768+| 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` |
769+| 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` |
770+| 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 |
771+
772+The [Agent preset](/guides/authentication/#scopes) does not include
773+`runners:read`.
774+
775+## Troubleshooting
776+
777+| What you see | What to do |
778+| --- | --- |
779+| 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. |
780+| *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. |
781+| `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. |
782+| `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. |
783+| `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. |
784+| *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). |
785+| *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). |
786+| 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`. |
787+| *… 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. |
788+| `sandbox up` says *WSL is not installed* | Open a terminal as administrator, run `wsl --install --no-distribution`, restart, and run `sandbox up` again. That is the one step that needs an administrator. |
789+| `sandbox up` says *WSL is set to mirrored networking* | Remove `networkingMode=mirrored` from `%USERPROFILE%\.wslconfig`, run `wsl --shutdown`, and try again. |
790+| `sandbox status` says *this sandbox can reach the computer* | Something of your computer's is mounted in the sandbox's VM, or an engine's socket is there; it lists what. Run `g1t-runner sandbox down --delete` and `up` again, which makes it from the image afresh. |
791+| `sandbox status` shows the Linux runner in `error` with *The runner stopped …* | The runner inside could not register or start; the message is its last line. Check the token was still valid (an hour), and that the computer reaches `https://api.g1t.sh`. |
792+| The `windows` capability is unavailable | `status` says why: Windows Sandbox is off, this is Windows 11 Home, or Windows is older than 24H2. |
793+| 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`. |
794+| *could not run docker* | Install Docker and start it, and give the runner's user access to it. |
795+| Agent work fails at once | Give the runner `--agent-image`. |
796+| 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`. |
+5−3
1111 import LinkCard from '../../components/LinkCard.astro';
1212 import Status from '../../components/Status.astro';
1313
14−**g1t is where your company's people and agents work together.** People and
15−agents are members of the same workspace: they talk in channels and direct
16−messages, and they sit on the same teams. You hand an agent work, it does it
14+People and agents are members of the same workspace: they talk in channels
15+and direct messages, and they sit on the same teams. You hand an agent work, it does it
1716 on a runner, and it comes back with something you can act on: a pull request
1817 to merge, a document, a reply to approve. **Home** tells you where you are
1918 needed and what happened while you were away. **Code** and **Deployments** are built in,
3736 <CardGrid>
3837 <LinkCard
3938 title="Quickstart"
39+ icon="rocket"
4040 description="Create a workspace, open Chat and talk to your first agent in a few minutes. Then put one on your code."
4141 href="/quickstart/"
4242 />
4343 <LinkCard
4444 title="Hire an agent"
45+ icon="bot"
4546 description="Pick a role, give it a name, a team and a voice, and set its budget."
4647 href="/guides/agents/#hire-an-agent"
4748 />
4849 <LinkCard
4950 title="Bring a repository"
51+ icon="git-branch"
5052 description="Push any git repository, or import one from another host, and your workflows run here."
5153 href="/guides/github/"
5254 />
+9−0
8181 Git and the API authenticate with a token. Create one in
8282 [Settings → Access tokens](https://g1t.sh/settings/tokens) and copy it: it is shown once.
8383
84+ <Tabs syncKey="os">
85+ <TabItem label="macOS and Linux">
8486 ```sh
8587 export G1T_TOKEN=g1t_…
8688 ```
89+ </TabItem>
90+ <TabItem label="Windows">
91+ ```powershell
92+ $env:G1T_TOKEN = "g1t_…"
93+ ```
94+ </TabItem>
95+ </Tabs>
8796
8897 2. **Push a repository**
8998
+4−0
3333 // MDX pages keep their markup; strip imports and leave the rest readable.
3434 const body = (page.body ?? '')
3535 .replace(/^import .*$/gm, '')
36+ // Tabs read as each tab's content under its label in bold.
37+ .replace(/^[ \t]*<\/?Tabs[^>]*>[ \t]*\n?/gm, '')
38+ .replace(/^[ \t]*<\/TabItem>[ \t]*$/gm, '')
39+ .replace(/^([ \t]*)<TabItem label="([^"]+)"[^>]*>[ \t]*$/gm, '$1**$2**\n')
3640 .replace(/^([ \t]*)<AgentSetup( variant="token")? \/>$/gm, (_match, indent: string, token?: string) =>
3741 agentsMarkdown(token ? AGENTS_WITH_TOKEN : AGENTS)
3842 .split('\n')
+27−0
606606 margin-top: 0;
607607 }
608608 }
609+
610+/* Tabs in a page, such as one per OS: quiet labels on a hairline, the
611+ chosen one in ink over a lavender line. */
612+starlight-tabs .tablist-wrapper {
613+ margin-bottom: 0.75rem;
614+}
615+starlight-tabs [role='tablist'] {
616+ gap: 0.25rem;
617+ border-bottom: 1px solid var(--g1t-line);
618+}
619+starlight-tabs [role='tab'] {
620+ margin-bottom: -1px;
621+ border-bottom: 2px solid transparent;
622+ padding: 0.4rem 0.75rem;
623+ font-size: 0.875rem;
624+ font-weight: 500;
625+ color: var(--g1t-muted);
626+ transition: color 0.15s, border-color 0.15s;
627+}
628+starlight-tabs [role='tab']:hover {
629+ color: var(--g1t-fg);
630+}
631+starlight-tabs [role='tab'][aria-selected='true'] {
632+ border-color: var(--g1t-accent);
633+ color: var(--g1t-fg);
634+ font-weight: 600;
635+}