/\_/\ ( o.o ) > ^ <
self-hosted git forge · single binary each side
rits is a headless git server. Clone, push, browse history, enroll build machines, and run CI — everything over one SSH port, driven from a terminal UI instead of a web app.
launching soon
rits is nearing its first public release. Want in? Send a one-line hello and you'll get one reply when the binaries drop — nothing else, ever.
a human reads this inbox · no list, no tracking
history Commits @ main Branches Jobs ✓ 8216de0 2026-08-19 remus │ commit 8216de0a3bfc4d8a33d46 ✓ e34cfe0 2026-08-18 remus │ Author: remus ✗ 50cbe45 2026-08-18 ada │ Date: 2026-08-19 10:27 ✓ 12a27bf 2026-08-18 ada │ ✓ Dockerfile.push@main j85c9ea87 ● 91ee5f9 2026-08-18 remus │ ✗ Dockerfile.tag@v* jb6ad7a77 fc289bf 2026-08-18 ada │ 1bf42a3 2026-08-18 ada │ ci: enable release checks 450 of 103,412 commits loaded • ↑/↓ select • b branches • esc back
one protocol
Other platforms bolt a web application onto git.
rits goes the other way: the git wire protocol is the whole
interface. Humans authenticate as git with their
public keys, build machines as runner — and every feature,
from creating a repo to streaming a build log, is an SSH command on
port 2222.
ci without yaml
Drop Dockerfile.push@main into
.rits/ and every push to main builds it. The build
is the job: its exit code is the verdict, its output
is the log, its image is the artifact. No pipeline DSL to learn —
you already know this file format. Several deliverables? Name the
hooks — Dockerfile.api.tag and
Dockerfile.worker.tag are two independent jobs on the
same trigger, and either can be a symlink to a Dockerfile your repo
already builds.
.rits/Dockerfile.push@main
FROM golang:1.25 AS build LABEL rits.runner="docker" WORKDIR /src COPY . . RUN go vet ./... && go test ./... RUN CGO_ENABLED=0 go build -o /out/app . FROM alpine:3.22 COPY --from=build /out/app /usr/local/bin/app
what a push sets in motion
$ git push origin main j85c9ea87 ✓ success push main push@main jb6ad7a77 ✗ failed tag v2.0 tag@v* j76d6ef0c ● running push main push@main j9c1330a2 ○ queued push feat… push@feature* artifact: rits/demo/push-main:8216de0 4 jobs • enter view log • r refresh
Jobs never clone — the build context arrives via
git archive. When a job does need a credential,
it's a per-repo secret: delivered as a BuildKit mount for one
RUN, never baked into layers, masked in logs.
Verdicts are written as git notes under
refs/notes/rits-ci — the ✓/✗ next to each commit
travels with the repo and works offline in plain
git log.
LABEL rits.runner routes jobs to matching machines.
No runner online? The job waits. A runner dies mid-build? The job
is requeued, up to three attempts.
runners
rits-runner is a single binary you put
on a VM, a Pi, a GPU box under the desk — or leave as the
runner service in the bundled compose file. It speaks to
the server over the same SSH port as everyone else, as a separate
identity class: runner, not git. No inbound
port on the build machine, no agent protocol, no web dashboard.
register on the server — token shown exactly once
Repos Runners ● docker docker 2026-08-12 ● pi-runner arm64,edge 2026-08-14 ◌ gpu-box gpu,cuda 2026-08-19 pending ○ laptop docker 2026-08-03 offline token for gpu-box (copy now — only its hash is kept): ritsr_9f2c4b8e1a7d3f60b5c2e8a4d1f7… n new • c copy • d revoke • esc back
enroll on the machine — then leave it running
$ rits-runner enroll --remote myserver:2222 \ --token ritsr_9f2c4b8e1a7d… generated ed25519 key ~/.rits-runner/id_ed25519 enrolled as gpu-box token burned server-side $ rits-runner run --remote myserver:2222 INF listening for jobs labels=gpu,cuda INF job j76d6ef0c demo push@main ● building INF job j76d6ef0c ✓ success artifact=rits/demo/push-main:8216de0 WRN connection lost — reconnecting in 2s… ok
Registering a runner mints a one-time enrollment token; the server stores only its SHA-256. The runner presents it as SSH password auth, generates its own ed25519 keypair, submits the public half, and the token is burned. From then on it's key auth like everyone else.
The runner role may fetch repos and listen for jobs —
nothing else. It cannot push, cannot create or delete repos, cannot
run admin commands. A compromised build box gets read access to
what it already builds, and no more.
A runner holds one persistent channel open and jobs stream down
it; that channel is what the TUI shows as ● online.
Drop and it reconnects with backoff. LABEL rits.runner
routes hooks to labelled machines — put the NVIDIA toolkit on a
box, label it gpu, and build steps see the GPU.
variables & secrets
Per-repo variables and credentials, set once from
any shell and handed to hooks at build time. Secrets are
encrypted at rest and never shown back — not in a listing,
not in a log, not in an image layer. Rule of thumb: if it would be
fine in docker history, it's a var; otherwise it's a
secret. Names starting with RITS_ configure rits itself
— which registry to publish an image to, which runner to build it
on — and never reach a build.
the TUI's vars view — v from commits
myrepo Commits @ main Branches Jobs Vars var NAS_URL http://nas:8080 var DEPLOY_ENV staging secret NAS_TOKEN •••••••• secret REGISTRY_PASS •••••••• edit NAS_TOKEN secret value: ▏ values are typed masked and never echoed back 4 entries • n new • enter edit • d delete • esc back
.rits/Dockerfile.tag@v*
FROM debian:12 AS build COPY . . RUN make deb ARG NAS_URL RUN --mount=type=secret,id=NAS_TOKEN \ TOKEN=$(cat /run/secrets/NAS_TOKEN) && \ curl -H "Authorization: Bearer $TOKEN" \ --upload-file out.deb "$NAS_URL/incoming/"
Variables arrive as ARGs. Secrets are mounted for a
single RUN via BuildKit and never touch
ENV or ARG — nothing persists into image
layers. Values are masked as *** in job logs.
Variables are public config and list with their values. Secrets
list by name only — the Vars view creates, edits and deletes them
without ever displaying one. The same operations exist as one-shot
commands: rits-cli var set, secret set,
vars.
A secret is encrypted the moment you set it and stays that way
until the one build that needs it. Rotating is a single
secret set; since nothing reads a value back, there is
nothing for a listing, a log or a layer to leak.
no server required
Not everyone wants to run a forge. rits-hooks
installs a git pre-push hook that builds the very same
.rits/ files on your own machine — no server, no runner, no
SSH. A failure stops the push. The trigger rules are shared code, not a
second implementation, so a green run here is a green job there if you
adopt a server later.
git push — the hook takes over
=^.^= myrepo Hooks Vars Artifacts main · 2 hooks ✓ Dockerfile.lint.push 3.7s ● Dockerfile.test.push ── Dockerfile.test.push #5 [2/2] RUN go test ./... #5 2.27 ok myrepo/internal/store 0.41s space mark · enter run this one · a all · q stop the push
two commands
$ rits-hooks install installed .git/hooks/pre-push 2 hook(s) fire for main $ rits-hooks --docs > .rits/GUIDE.md # the whole hook reference, printed by every rits binary
Values live in a hidden ref inside the clone, sealed with a key in your config directory — same design as the server's, so a hook that mounts a secret works locally too.
What a build declares lands in .rits-artifacts/,
permissions intact — the .deb you just built is right
there, executable.
Nothing in .rits/ is local-only. Point the repo at a
rits server when you want shared runners, and the same files run
there unchanged.
roadmap
Working today, verified end to end.
.rits/Dockerfile.[<name>.]<event>[@glob]; branch and tag triggers, several named hooks per event; builds as sibling containers, never DinD.rits/repo/hook:sha, newest three kept, older pruned..deb, a disk image, a binary — uploaded to the server, browsable and downloadable from the CLI and the TUI, permissions intact.rits-hooks runs the same .rits/ hooks from a git pre-push hook: a failing build stops the push, with its own encrypted variables and artifacts on disk..deb with all four tools and a systemd unit each, configured from a single env file; installing enables nothing.Directions under consideration, in rough order.
Ready to use — self-hosting only. You bring the box, the keys, and the runners. There is no hosted offering, no account system, no telemetry — your repositories never leave hardware you control.