Skip to content

The Toolchain (.bin/b.yaml)

Every lok8s project carries a pinned toolchain in .bin/. The file that declares it is .bin/b.yaml, managed by b, a binary manager and env-file syncer (binary.help). lok8s itself is distributed as a b environment: b installs the lo CLI, the .lok8s/ framework tree, and every tool the framework calls.

This page documents what b does in a lok8s project and the b.yaml fields lok8s actually uses. For the full b feature set, see the upstream docs.

Documented against b v4.18.x

b ignores keys it does not know, so a field name from a different version fails silently: the binary still installs, just with default behavior. If something here does not match what you see, check binary.help for your version.

How b fits the lo workflow

b owns two things in a lok8s project:

  1. Binaries: the lo release binary, kubectl, kustomize, sops, tilt, kind, and the rest land in .bin/, pinned per project. Nothing touches your system.
  2. Framework files: .lok8s/** (addons, driver templates, provider plugins, the frozen argsh CLI), the Tilt extension, the kustomize plugins, and .bin/b.yaml itself sync from the upstream lok8s repo.

The commands you run:

bash
# A new project: the files (lo init project), then the toolchain: lo writes
# .bin/b.yaml, installs b into .bin/ and runs b install (new or existing project)
lo init project
lo toolchain install
lo toolchain doctor        # verifies the pins landed

# Join an existing lok8s project: install the exact pinned toolchain
.bin/b install             # (or `b install` with b on PATH)

# Pull binary upgrades for the unpinned entries (the pins stay)
.bin/b update

# CI: exit non-zero when anything is out of date
b version --check

# Verify installed artifacts against b.lock checksums
b verify

# The profile path — the framework tree + the frozen bash reference too
b env add github.com/kernpilot/lok8s#local && b install

b install reads .bin/b.yaml and writes b.lock (versions + SHA256), so teammates and CI get the identical toolchain. Getting Started walks through the same commands, after installing the lo release binary.

lo toolchain install: the consumer file

A consumer project does not need the whole profile: lo toolchain install writes a .bin/b.yaml generated from the pins the running lo was built with, then bootstraps b and runs b install (lo init project writes the project files only and prints this command as the next step):

yaml
# my-project — the lok8s toolchain, managed by b (github.com/fentas/b · binary.help).
# lo-toolchain: managed pins — written by `lo toolchain install`
# Generated by lo v0.5.0 (core); `lo toolchain install` never overwrites this file, it
# prints a diff. Run `.bin/b install` to (re)install; `lo doctor` verifies what landed.
# … (the header explains the pins, the groups and how to add the bash-side tools)
binaries:
  # ── core: what `lo` execs on every path
  kubectl:
    groups: [core]
  kustomize:
    version: v5.8.1
    groups: [core]
  github.com/mgoltzsche/khelm:
    version: v2.8.0
    file: ../.kustomize/khelm.mgoltzsche.github.com/v2/chartrenderer/ChartRenderer
    groups: [core]
  github.com/kernpilot/lok8s:
    version: v0.5.0                      # this lo's own version
    asset: kustomize-secret-*
    file: ../.kustomize/secrets.lok8s.dev/v1/secret/Secret
    groups: [core]

  # ── local: the kind + Tilt dev loop (lo up, lo tilt, lo trust)
  kind:
    groups: [local]
  tilt:
    groups: [local]
  mkcert:
    groups: [local]

  # ── cloud: the provisioning drivers (KubeOne on Hetzner) — opt-in
  # github.com/kubermatic/kubeone:
  #   groups: [cloud]
  # hcloud:
  #   groups: [cloud]
  • The pins are drift-tested. kustomize v5.8.1 is the CLI release built from the kustomize API the lo-full build links (v0.21.1); khelm v2.8.0 is the library lo-full links and the ChartRenderer binary lo core execs; the secrets.lok8s.dev Secret plugin is the kustomize-secret-<os>-<arch> asset of this lo's own release. All three live in internal/toolchain/pins.go and go test fails when go.mod or the template disagrees. The exec render (core) and the in-process render (full) stay byte-identical because the numbers cannot drift apart.
  • Groups are a lok8s convention (b ignores the key): core is always on, local by default, cloud (kubeone + hcloud) on --groups core,local,cloud. Entries outside the selection are emitted commented out: uncomment and .bin/b install.
  • Never overwritten. An existing .bin/b.yaml stays byte-for-byte; lo toolchain install prints a unified diff against the template and the instructions, and lo toolchain doctor reports which installed tool differs from a pin.
  • Not in the consumer file: argsh, yq, jq, envsubst, sops, ssh-to-age. The Go binary links or reimplements them (sops and ssh-to-age as libraries; the split-mode yq/sops subprocesses are contributor territory today, and the tree a project routes commands to). To run the frozen bash implementation in a consumer project, add (as the file's header says) github.com/arg-sh/argsh: {asset: argsh, onPost: "${B_BIN} builtin ${B_EVENT}"}, yq: {}, jq: {}, renvsubst: {alias: envsubst}, sops: {}, ssh-to-age: {} and run .bin/b install.
  • b itself is installed into .bin/b from its pinned release tarball (b-<os>-<arch>.tar.gz, the asset b's own installer resolves), verified against the SHA-256 recorded from that release's checksums.txt before extraction. No curl | sh. --dry-run shows the URL and the sum. GITHUB_TOKEN is passed through when set (public sources need none). b publishes no darwin build: on macOS install b by hand (binary.help) and re-run.

What b manages: consumers vs contributors

Two files, two audiences. The consumer file is what lo toolchain install writes (above); the contributor file is the lok8s repo's own .bin/b.yaml, which the profiles publish and which still carries the bash side.

Binary (group)Consumer (lo toolchain install)Contributor / profilesWho needs it
kubectl (core)yesyeslo execs it for apply/wait
kustomize v5.8.1 (core)yes, pinnedyeslo core: every render. lo-full: only LO_RENDER=exec; the bash side always
khelm ChartRenderer v2.8.0 (core)yes, pinned, under .kustomize/yesthe addons' Helm charts (core execs it; lo-full links the same release)
secrets.lok8s.dev Secret plugin at lo's version (core)yes, pinned, under .kustomize/built from source (lo kustomize build)every render on core and the bash side; lo-full serves it itself
kind, tilt, mkcert (local)yes (default on)yesthe local dev loop: lo up (kind), Tilt, lo trust (mkcert)
kubeone, hcloud (cloud)opt-in (--groups …,cloud)kubeone/capi profiles (+ clusterctl)the provisioning drivers exec them
dockerno (install Docker yourself)oci://dockerthe Lo driver and Tilt
argsh, jq, yq, envsubst, sops, ssh-to-ageno: the binary links or reimplements them (add them by hand when lok8s.yaml routes commands to bash; the file's header shows how)yesthe frozen .lok8s/lo, the provider plugins, lo build split mode (yq/sops subprocesses), the bats suites
batsnoyescontributors running tests/
lo itselfno — you installed it (lo-install.sh)the core profile declares the release asseteveryone

For a consumer the file above is the whole toolchain; lo doctor tells you when a pin is missing or off. For a contributor everything in the right column is needed, because the parity harnesses and the bats suites really run the argsh side; b install in a clone of the repo gets it.

Intended end state (planned; each step is sequenced separately): argsh, jq and bats leave the contributor core group once the provider plugins have Go twins and lo chat drives the Go lo mcp start (the shipped .mcp.json already does); sops and ssh-to-age follow once lo build split mode encrypts through the library; yq stays until the renderer-drift rule in The Go lo binary is satisfied. kustomize, khelm, kind, tilt, mkcert, kubectl and the driver CLIs stay: they are the tools lok8s orchestrates, not implementation details, and on lo core the first two are the renderer.

Install directory

b picks the install directory from the first of these that is set: PATH_BIN, then PATH_BASE, then <git-root>/.bin, then <cwd>/.bin. Note that b uses PATH_BIN and PATH_BASE verbatim: it does not append .bin to them, it only does that for the git-root and working- directory fallbacks. The .envrc that ships with every profile exports these for direnv users.

Authentication

Public sources need no token. Set GITHUB_TOKEN only for private repos or to raise GitHub API rate limits.

The binaries section

binaries is a map. Each key is either a pre-packaged name (kubectl, jq, sops, …: b search <name> lists them) or a provider ref (github.com/arg-sh/argsh, oci://docker, go://…, git://…). An empty value {} means "latest, defaults".

Fields lok8s uses, from the real .bin/b.yaml:

yaml
binaries:
  oci://docker: {}                  # docker CLI from an OCI image, daemonless

  renvsubst:
    alias: envsubst                 # install under a different name
    groups: [core]                  # profile tag (see below)

  github.com/arg-sh/argsh:
    asset: argsh                    # pick this release asset by glob
    groups: [core]
    onPost: "${B_BIN} builtin ${B_EVENT}"   # hook after install/update

  github.com/mgoltzsche/khelm:
    file: ../.kustomize/khelm.mgoltzsche.github.com/v2/chartrenderer/ChartRenderer
    groups: [kustomize]             # custom install path (relative to b.yaml)
FieldPurpose
versionPin a version (tag). Without it, b installs the latest and b update upgrades.
aliasInstall the binary under a different name on PATH.
assetGlob that selects one release asset when a release ships several.
fileCustom install path, relative to the b.yaml location. lok8s uses this to place kustomize exec plugins under .kustomize/.
onPostShell hook that runs after a successful install or update, only when the binary on disk changed. Gets B_EVENT (install|update), B_NAME, B_VERSION, B_FILE.
groupsNot a b field. A lok8s convention: b preserves unknown keys, and the profile select expressions below filter on this tag.

The profiles section

profiles is b's env-sync feature: an upstream repo publishes named file sets, and consumers subscribe with b env add github.com/kernpilot/lok8s#<profile>. lok8s publishes five:

ProfileIncludesAdds
core(none).lok8s/**, .envrc, .gitignore, .mcp.json, skills, and the core-tagged binaries
kustomize(none).kustomize/** plugins and their binaries
localcore + kustomizeTiltfile, services.yaml, kind/Tilt/mkcert/bats
capilocalclusterctl, hcloud
kubeonelocalkubeone, hcloud

Each profile entry has a description, optional includes (compose from other profiles), and a files map of glob patterns to sync. One pattern deserves a note. The profile syncs a filtered b.yaml:

yaml
profiles:
  core:
    files:
      .lok8s/**:
      .bin/b.yaml:
        select:
          - "{binaries: from_items(items(binaries)[?[1].groups && contains([1].groups, 'core')])}"

select extracts keys from a YAML file instead of syncing it whole; a JMESPath expression here keeps only the binaries tagged with the profile's group. The effect: a core consumer's .bin/b.yaml lists only the core binaries. Each profile ships the tools it needs and nothing else.

You rarely touch profiles as a consumer. b env add copies the resolved profile into your local b.yaml, and b update keeps it in sync.

Adding your own tools

Your project's .bin/b.yaml is yours after sync. To add a tool:

bash
b install --add github.com/derailed/k9s     # install + record in b.yaml
b install --fix jq@1.7                      # install + pin the version

Or edit .bin/b.yaml directly and run b install. Commit b.yaml and b.lock so the whole team gets the same tool.

See also

Released under the MIT License.