Skip to content
bay
How it worksFleetRoadmapDocsBlogGitHub ↗
Early access
Docs menu

Start here

  • Overview
  • Install
  • Quickstart
  • Concepts

Land changes

  • Executors
  • Undo a landing
  • Configuration

Coordinate agents

  • Agents and hooks
  • See what your fleet is doing
  • Talk to other actors

Operate bay

  • Host a trunk without a forge
  • Keys and the registry
  • Update

Reference

  • CLI reference
  • Results and refusals
  • Changelog

Start here

  • Overview
  • Install
  • Quickstart
  • Concepts

Land changes

  • Executors
  • Undo a landing
  • Configuration

Coordinate agents

  • Agents and hooks
  • See what your fleet is doing
  • Talk to other actors

Operate bay

  • Host a trunk without a forge
  • Keys and the registry
  • Update

Reference

  • CLI reference
  • Results and refusals
  • Changelog

Operate bay

Host a trunk without a forge

Keep trunk in a plain bare Git repository over SSH, and let the host itself refuse any rewrite of trunk.

bay is in development. These pages describe bay as it will ship.

bay host init creates a bare Git repository to serve as your remote. Its trunk is append-only, and Git on the host enforces that on every push. With a key registry, the host also refuses any trunk commit that is not an executor's landing. Use it when you have no forge, or no route to one.

It is plain Git, not a server. bay adds no protocol, no accounts and no daemon. Git over SSH carries every push and fetch. Who may push at all stays with SSH and the host's file permissions.

When to host a trunk

  • No forge: you keep this repository on no GitHub, GitLab or other forge.
  • Air-gapped machines: the machines have no route out. Move changes with bay share --bundle.
  • A bare repository over SSH: you already keep repositories on a server you run.
  • Rules on the host: you want the host itself to refuse a rewrite of trunk.

Set up the host

  1. 01

    Check Git

    The host needs Git 2.54 or newer. Older Git ignores hooks that live in configuration. Below 2.54, bay host init refuses with GIT_BELOW_FLOOR and creates nothing.

  2. 02

    Create the repository

    Run bay host init on the host. The path must be empty or absent. --trunk names trunk, main by default. --object-format sha256 makes a SHA-256 repository.

    sh
    bay host init /srv/git/app.git --trunk main
  3. 03

    Check the host

    Run bay doctor in the new repository. Its trunk-hook row must be green.

    sh
    cd /srv/git/app.git && bay doctor
  4. 04

    Point your clones at it

    Add the host as a remote, push trunk, and name that remote in .bay/config.toml.

    sh
    git remote add origin ssh://git.example.com/srv/git/app.git
    git push origin main
  5. 05

    Add the registry

    Push the first trunk before the first registry. With a registry, each new trunk commit must be an executor's landing, the first ones too. The Keys page shows how to write the registry.

What bay host init sets

The hooks call the bay that ran bay host init, by its full path. Keep bay at that path on the host.

SettingWhat it does
--ref-format=reftableStores refs in reftable, whose updates of several refs in one push are atomic.
hook.bay-trunk on updateKeeps trunk append-only.
hook.bay-scope on pre-receiveJudges every bay ref in a push, once the host holds a key registry.
bay.trunkThe trunk name that both hooks read.
core.logAllRefUpdates = alwaysKeeps a reflog for every ref. The hook reads it to judge an adoption by the host's clock.
receive.advertiseAtomic = trueLets a landing move trunk and its landed marker in one atomic push.

What the trunk hook refuses

  • A push that is not a fast-forward of trunk.
  • A deletion of trunk.
  • A new trunk after the old one held landed changes.
  • Any trunk update the hook cannot judge. An unknown outcome is a refusal.

What stays rewritable

The trunk hook judges trunk only. refs/bay/* and refs/notes/bay/* stay rewritable, so retention cuts still work. So bay does not set receive.denyNonFastForwards or receive.denyDeletes. Those would block retention cuts too.

With a key registry

Once the host holds refs/bay/registry, the scoped hook judges every push. Git does not tell a hook who pushed. So the hook judges the signatures on what the push adds. Any refusal fails the whole push.

  • Only an executor key can move trunk. Each new trunk commit must be an executor-signed landing.
  • Each landing needs a current agent-signed request and passing producer-signed evidence.
  • Each landing must create its landed marker in the same push, and a change lands only once.
  • Only a key in the current registry file can advance refs/bay/registry, and only by fast-forward.
  • A live ref takes only a summary of its own machine, signed by the agent key it names.
  • Every bay ref name must use bay's fixed alphabet.

Without a registry, the host keeps only the append-only trunk. Anyone who can push may then add commits to trunk.

Compared with a forge

RuleOn a trunk from `bay host init`On a forge
Trunk is append-onlyThe trunk hook refuses a rewrite or a deletion.Turn on the forge's branch protection for trunk.
Only landings reach trunkWith a registry, the scoped hook refuses any commit that is not an executor's landing.Let only the executor's identity push to trunk.
A revoked keyRefused on every later push.bay's own checks apply it after their next fetch.

Share a change

bay share hands a change and its evidence to another actor. It pushes refs/bay/changes/<change> and every evidence notes ref to a remote. --remote names that remote. The default is the remote in .bay/config.toml. --bundle writes the same refs to one file instead, for a machine with no route. The other actor fetches the refs with plain Git, then runs bay start --resume <change>. No bay test covers the bundle path yet, so check the fetched refs before you rely on them.

sh
bay share c7k2m4 --bundle c7k2m4.bundle

# on the other machine
git fetch c7k2m4.bundle 'refs/bay/changes/*:refs/bay/changes/*' \
  'refs/notes/bay/evidence/*:refs/notes/bay/evidence/*'
bay start --resume c7k2m4

Refusals

bay host init refuses below the Git floor. The host refuses a push with the reason on stderr.

CodeMeaning
GIT_BELOW_FLOORGit on the host is older than 2.54. bay host init created nothing.
HOOK_TIMEOUTThe scoped hook ran past its time bound and refused the push. bay retries the same candidate.
QUEUE_HELDAnother operation holds the head of the queue, so this landing waits.
REGISTRY_CHANGEDThe registry revoked a key behind this landing after the executor signed it.
HOOKS_MISSINGbay doctor found a bare repository with no trunk hook.

Not built yet

  • No bay command writes refs/bay/registry. A registry-key holder edits it by hand.
  • On a forge, bay does not yet report a trunk commit that did not come through bay.
  • Compacting old landed markers into index refs once a repository holds many bay refs.

Limits

  • Git on the host: 2.54 or newer.
  • Hook time: the scoped hook judges a push for at most 30 seconds by default. Set bay.hookTimeoutS on the host.
  • Live refs: a host holds at most 256 live refs.
  • Operator names: machine, executor and producer names use a-z, 0-9 and -, at most 32 characters.
← PreviousTalk to other actorsNext →Keys and the registry

On this page

  • When to host a trunk
  • Set up the host
  • What bay host init sets
  • What the trunk hook refuses
  • What stays rewritable
  • With a key registry
  • Compared with a forge
  • Share a change
  • Refusals
  • Not built yet
  • Limits
how it worksfleetroadmapblog© 2026 bay contributors · MIT
Built by Gudjon Gudjonsson, founder and CEO of OZ →Sponsored by OZ