latem, home

CI on the machines you already own.

The laptop on your desk, the box under it, the server you pay for anyway. They are faster than what you rent by the minute, and already set up the way your project needs. Latem is the plumbing that lets them build unattended.

$ curl -fsSL https://raw.githubusercontent.com/Abyssinia-Labs/latem/main/install.sh | sh

v0.1.2 · Apache-2.0 · Linux and macOS · signed releases

running · 01:42 elapsed · mac mini

checks

latem-cloud @ refs/pull/6/merge · 73fb3c6 · linux runner · no minutes billed

  1. Queue
  2. Prepare
  3. Run
  4. Collect
  5. Done
19[latem] step 3/7: step 3
20$ biome check .
21Checked 140 files in 195ms. No fixes applied.
22[latem] step 3/7: step 3 finished in 406ms
23[latem] step 4/7: step 4
24$ bun --filter '*' typecheck

Hosted CI rents you a machine, then throws it away.

That one fact is where the cost and most of the ritual come from. A cache becomes a save step, a restore step, a key to get wrong and a ceiling to hit, because the machine that warmed it won't be there next time. Anything that takes real setup, a toolchain, a device, a signing identity, is rebuilt every run or handed to someone else's computer to keep.

So the compute isn't the missing piece. The plumbing is.

Buildkite, Woodpecker, Drone, Forgejo Actions and Actions Runner Controller already run on hardware you own, and latem doesn't try to replace them there. It does the part they leave to you: the machine, the cache, and the threat model.

Everything between having a machine and trusting it with a build.

01

Your machine is the build machine.

Two executors behind one interface. Docker isolates a job to its workspace and the caches it declared. Native runs on the host as the agent's user: the Xcode you have, the keychain you have, the devices plugged in, the licence tied to the machine.

Which one is a question of whose code runs here, not how big your team is. A job picks a runner by label, so moving a machine between the two is a config edit.

Linux and macOS. Windows runs Docker jobs; native Windows isn't built yet.

terminallatem setup
$ latem setup
1/4 config
  wrote /Users/me/.latem/config.toml
  runners: macos (native), linux (docker)
02

Caches are mounted, not archived.

Your machine is the same machine every time, so a cache is a directory that is still there. No tarball, no upload, no key, no size ceiling, no cache step. ~/.gradle is simply where you left it.

A workflow's actions/cache becomes a mount without being edited. Caches belong to one runner, and a runner taking two jobs at once locks each cache, so two builds never write the same store.

.latem/jobs.yamldeclared once
# mounted into every job that names it
caches:
  - name: pnpm-store
    path: /home/runner/.local/share/pnpm/store

jobs:
  test:
    caches: [pnpm-store]
03

Five invariants, written down and tested.

A self-hosted runner is a machine on your network, holding your signing certificates, running what a webhook asked for. Latem's threat model is a document with a test named after each rule. A pull request that weakens one is rejected, whatever else it does.

RUNNER_SPEC §5, the full statement

  • INV-1Outbound only. No listener, no port to forward, no firewall rule. The CLI talks over a 0600 Unix socket.
  • INV-2Names, never definitions. The control plane can send run and cancel. A run has no field that can carry a command, an image, a path or a timeout.
  • INV-3Nothing outside reaches a shell. A ref named --upload-pack=/bin/sh is a ref.
  • INV-4Secrets never leave the machine. Redacted where they are emitted, line by line, so a PEM key is redacted too.
  • INV-5Jobs come from your commit. Read from the checked-out workspace, never fetched. Refs outside your allowlist are rejected.

Your workflows, run where you say.

Install the latem-ci GitHub App and link it at app.latem.dev. A pull request's checks run on your machines and report back to the pull request. A push, a release or a dispatch runs only the workflows you switched on, and a deploy can wait for you to approve it.

actions/checkout is a step latem performs. actions/cache becomes a mount. Reusable workflows, services: and container: are refused by name, not half-run.

Before any of it, latem workflows check reads your .github/workflows offline and says, job by job, what stands in the way and the fix.

terminaloffline, no account
$ latem workflows check ~/src/latem-cloud
checking /Users/me/src/latem-cloud

.github/workflows/checks.yml  "Checks"
  planned for: push to main, pull_request
  [FAIL] checks
     - uses oven-sh/setup-bun@v2: not in [actions] allowed
         fix: [actions] allowed += "oven-sh/setup-bun"
     - runs-on "blacksmith-4vcpu-ubuntu-2404":
       no runner here carries it
         fix: [label_aliases]
              "blacksmith-4vcpu-ubuntu-2404" = ["linux"]

0 of 1 jobs can run on this machine

Your machine dials out. Nothing dials in.

GitHub

the git host

A push, a pull request, a release or a dispatch. Checks appear on the pull request.

→↓webhookchecks back

app.latem.dev

the control plane, optional

Plans the workflow, holds the queue, asks you before a deploy. It sends names: run, repo, job, ref, inputs. Never a command.

←↑the agent dials outwss, outbound

The agent

on your machine

Reads the job from the commit it checked out. Secrets and caches stay here. No listener, no open port, no address the control plane knows.

Or none of the middle: latem run myapp test needs no account, no hook and no network. A machine that never registers is a supported machine, not a trial.

Every failure names its fix.

Almost every way a self-hosted runner fails is environmental, and almost none of them say so. doctor checks thirty-odd things and prints the command that repairs each one.

A git credential a service can use with no terminal to prompt at. Mirrors and work directory on one filesystem, so clones hardlink instead of copying gigabytes. A Docker image that will not fail forty minutes in. FileVault, before an unattended reboot stops at the login screen.

terminallatem doctor
$ latem doctor

Tooling
  [ ok ] git              git version 2.50.1
  [ ok ] docker daemon    reachable
  [skip] tart             not installed, no runner uses it

Disk
  [ ok ] free disk / ios  394.7 GB free, floor 60 GB
  [ ok ] mirrors and work_dir  one filesystem

macOS session
  [warn] FileVault        on: an unattended reboot
                   stops before login
         fix: sudo fdesetup authrestart

18 passed, 2 warning(s), 4 failed, 3 skipped

Move one job, or all of them.

Beside what you have

Move the long, expensive, awkward job onto a machine you own and leave the rest where it is. A hook binds one repository and one ref pattern to one job on one machine. Your existing CI never learns about it.

Instead of what you have

Take all of it: your GitHub workflows run on your machines through the latem-ci App, or locally with no account at all. Same binary either way, and going from one to the other installs nothing new.

Tested, not argued.

The hardest version of self-hosted CI is a Mac: an iOS release needs a real user session, an unlocked keychain, signing identities and a simulator that matches the SDK. If a runner can do that unattended, it can do the rest.

2026-08-11

The agent, started by launchd rather than a shell, took ios-release from a bare clone to a signed build in TestFlight.

INV-4, on that run

No signing password, no issuer id and no line of the App Store Connect key appears in the run log, any other log, or the store.

a second machine

The same runbook on a Mac that had never seen latem found three defects the first could not.

not built yet, and said so

A 1.0: a flag may still be renamed. The Tart VM executor. A reboot with nobody logged in. Native Windows.

A clean machine to a green build in under five minutes.

Never as root. The script checks the download's sha256 and, with minisign installed, its signature; installs to ~/.local/bin; and runs latem doctor. Then latem setup writes a config, registers the machine with a code you approve in the browser, installs the service and checks the result. It is safe to run again.

$ curl -fsSL https://raw.githubusercontent.com/Abyssinia-Labs/latem/main/install.sh | sh
$ latem setup
$ latem repo add me/myapp

release key · RWTTZK1Cxjg51w6BUpg5KpkKhOHzUT+EDhgidVqHUHsiTr6nPjKC3esu

terminallatem setup
$ latem setup
1/4 config
  wrote /Users/me/.latem/config.toml
  runners: macos (native), linux (docker)

2/4 register
to register this machine, open

  https://app.latem.dev/connect?code=WDJB-MJHT

and approve the code  WDJB-MJHT
waiting for approval: the code expires in 10m0s …
  approved
verifying wss://gw.latem.dev/agent …
  connection verified

3/4 service
4/4 doctor