Configuration
~/Library/Application Support/Kinhin/config.yaml — write the commented starter file with kinhin config init. Key fields:
| Field | Meaning |
|---|---|
version | Config schema version (optional, absent = 1). Unknown keys are an error at version 1; see Validation |
accounts | The forges this fleet serves, each with its own token: name, forge (github, gitlab, gitea, forgejo; azuredevops is experimental; buildkite and bitbucket are planned and not supported yet), scope (owner/name for one repo, or a bare org name for org-level runners that serve every repo in the organization; PAT needs org runner-admin permission), and instance for self-hosted forges — see Forges. There is no top-level repo: key |
labels | Jobs must request all of these to be picked up |
min_runners / max_runners | Scaling bounds (in VMs). Defaults 0 / 2. min_runners is one floor for the whole fleet: busy instances count toward it and the instances it adds run on runtimes.default (see Scaling) |
image_min_idle / min_idle | Registered, idle runners kept at all times so the forge always lists one: image_min_idle for the top-level image:, min_idle on each images: entry, on container: and on docker:. A busy runner does not count, so a replacement starts as soon as one takes a job. Default 0 (off); see Idle runners per pool and image |
container | Linux jobs in Apple container microVMs: image, base_image, cpus (4), memory_gb (8), labels, fleet_labels, min_idle, docker (false; true runs Docker inside each runner, pulling Docker Hub images through kinhin's cache, and { cache: false } runs Docker without the Docker Hub cache), flags (extra container run arguments), platform (linux/arm64; linux/amd64 runs x86_64 runners under Rosetta, registered with x64) and pools (named pools: each takes those keys plus max, the top-level ones are their defaults). See apple/container and Several container pools |
warm_pool / warm_max_age_minutes | Pre-booted, unregistered Tart VMs kept for instant pickup (per image: also warm_pool on each images: entry), and when to recycle them. Defaults 0 (off) / 60 |
runners_per_vm | Runner processes per VM — many-to-one job density (1–8; 1 = one job per VM, most isolated) |
poll_interval | Seconds between forge polls (≥ 5, default 20) |
vm.cpu / vm.memory_gb / vm.ephemeral | Per-VM shape |
image | Tart image cloned per runner (tahoe-base after prepare) |
base_image | What image prepare starts from: a pre-configured tart image (ghcr.io/cirruslabs/…, macOS or Linux), digest-pinned (ref@sha256:… — see kinhin image digest), or a local VM name (e.g. one made with image prepare --linux); absent = the default macOS base. A Linux base turns the Tart pool into Tart Linux VMs |
provision_script | Host script baked into the image by image prepare (your toolchains; see Toolchains) |
projects | Per-repo rules (pattern, repo_file, toolchains, share_cache): repo_file: true lets a repo's .kinhin/config.yml (or older .kinhin.yml) choose its toolchains; toolchains declares them here instead. There is no global toolchains: (see Per-project toolchains) |
toolchain_cache | Host download-cache expiry: auto_prune, max_age_days (30), max_gb (50). max_project_images (8) caps derived images |
reserve.cpu / reserve.memory_gb | Host headroom kept free |
reserve.disk_gb | Host disk (GB) kept free (default 10) |
healing | The healing agent: enabled (true), auto (safe, or none to approve every fix), ai (auto or off), max_attempts (3, 1–10). See The healing agent |
telemetry | Opt-in observability, off by default: enabled, endpoint, protocol (http/protobuf or grpc), export_interval_seconds (60), signals, resource_attributes, prometheus_listen, job_attributes, log_format (text or json), sampling_ratio (1), log_level (info), headers_keychain, ca_file, persist (false). Standard OTEL_* variables override it. See Telemetry |
Scale-set mode (GitHub, opt-in): an accounts: entry may carry scale_set: with name (the scale set's name — what workflows put in runs-on:; kinhin adds -container, -docker and -<image> sets for the other pools and named images) and runner_group (where the scale set lives; GitHub's built-in group is Default, spelled exactly that way, and kinhin's lookup is case-sensitive). It swaps that account's demand from the REST poll to the message API — see Forge support and Troubleshooting scale sets.
The effective concurrency cap is min(max_runners, (cores − reserve.cpu) / vm.cpu, (RAM − reserve.memory_gb) / vm.memory_gb, free_disk / disk_per_vm) — disk_per_vm is your disk_gb when set, else a runtime default (50 GB for Tart VMs, 8 GB for Linux container slots). The scaler re-checks free disk before every spawn and holds new VMs when the disk would dip below reserve.disk_gb plus one instance's footprint. kinhin doctor prints the result and the binding constraint.
Validation and defaults
config.yaml is validated when it loads, and errors name the key as written in the file (vm.memory_gb, not an internal name).
- Unknown keys are an error. A typo such as
max_runner:is rejected with a suggestion (did you mean 'max_runners'?) instead of silently falling back to the default. The check covers the top level and the nested sections (vm,reserve,container,docker,runtimes,images,accounts,projects,toolchain_cache,healing,telemetry). Top-level keys that start withx-are ignored, so you can keep notes or YAML anchors there. versionis optional and defaults to1. A file that declares a newer version than your kinhin understands is still read: keys it does not know are logged and ignored, and the keys it does know are validated as usual. That lets an older build run a config written for a newer one.- Ranges.
vm_boot_timeoutmust be 30–3600 seconds andtoken_ttl60–3600 seconds, on top of the bounds onpoll_interval,runners_per_vmand the resource keys. container.flagsmust not set what kinhin sets itself (--name,--rm,--detach,--cpus,--memory,--platformand--arch(both come fromplatform),--os,--help,--versionand a bare--, in long,--flag=valueand short forms), and each element must be non-empty with the first one a flag. See Extracontainer runflags.container.pools. Pool names are lowercase letters, digits and inner-(at most 32 characters); every pool needs an image (its own orcontainer.image); no two pools may have the same labels and the sameplatform, and pools that share an image must share itsbase_imageandplatform;platformislinux/arm64orlinux/amd64;maxmust be at least 1 and the pool'smin_idle; each pool's keys are checked like the same keys oncontainer:. See Several container pools.min_idle. Everymin_idle(andimage_min_idle) must be 0 or more and fit inmax_runners, each image's together with itswarm_pool, and all of them together too;docker.min_idlemust also fit indocker.max_slots. The values on macOS Tart images may add up to at most 2, Apple's limit of macOS VMs per Mac.- Defaults come from one place, so omitting a key and the starter template agree:
min_runners: 0,max_runners: 2,runners_per_vm: 1,poll_interval: 20,vm: {cpu: 4, memory_gb: 8},reserve: {cpu: 2, memory_gb: 4, disk_gb: 10}. - Runtime names in
runtimes:(default,budget,routes[].runtime) aretart,appleContainer,dockerandhost; anything else is rejected with a suggestion.
Older configs
The top-level repo:, gitea: and forgejo: keys and the global toolchains: section no longer load; the error names the replacement (an accounts: entry, a projects: entry, toolchain_cache:). ConfigMigrator in KinhinKit translates them as text without writing the file, keeping comments elsewhere in it.
Moving to another Mac
Copy a working setup to a second Mac (or a new one) with one command per side:
kinhin config export kinhin-config.yaml # on the Mac that has the config
kinhin config import kinhin-config.yaml # on the new MacOr in one step over SSH: kinhin config export | ssh other-mac kinhin config import - --yes.
- Tokens are not copied. They stay in each Mac's Keychain. The import lists every account that has no token on the new Mac, with the
kinhin auth set --account NAMEcommand to store it. - Images are not copied. Each Mac bakes its own;
kinhin setup statusshows which to prepare. - Nothing is written until the file validates. With no config yet the import writes it; an existing config is only replaced with
--yes, and the old one is kept asconfig.yaml.bak. - The export starts with three
# kinhin export:comment lines naming the source Mac; the import drops them. Atart_paththat does not exist on the new Mac is reported as a warning. - A running fleet picks up the new config after you restart the app or the daemon.
Live reload
Config edits made from the app's Advanced → Routing tab (routes, budget split, labels) reach a running fleet live via reload_config — no restart; a running fleet simply re-buckets demand on its next poll. Routes can also be edited from the terminal:
kinhin runtimes route add acme/api appleContainer
kinhin runtimes route remove acme/apiEvery pane edit rewrites only the relevant section of config.yaml — comments elsewhere are preserved — and lands only if the updated file still passes the engine's full config validation.
