forge guide

a personal git forge

forge

Your code, its reviews, CI results, sites and releases, on your own server, as a static site. Changes go through git and ssh.

start readingcheat sheetinternals

$ forge attest --stackafd124b running .forge/ci: parse --nameafd124b pass in 1s, signed80b9dda running .forge/ci: add --shout80b9dda pass in 2s, signed$ forge push -t "Flags"remote: review #2: https://src.rest/cat/greet/reviews/2/$ forge site deploy docssite docs: deployed 942ff39 -> https://greet-docs--cat-flags.pages.src.rest/

01 what forge is

Your forge, on your own server

forge keeps your repositories on your own server and publishes them as a static site: every branch, file and commit, the reviews on them, the CI result of every commit, and each repo's sites and releases. The pages work without JavaScript and update while you read them.

Changes go through git and ssh. You push code, reviews, sites and releases; you run an ssh command to make a repo, land a review or invite someone. On the web you read. Sign in with a passkey to comment and approve. A push that adds a password or a key is refused.

what you do, and what you get
youwiththe forge shows
push codegit pusha page for every branch, directory, file and commit
push a branchforge pusha review, with a page for each revision
run CIforge attesta signed pass or fail on each commit
publish a siteforge site deploythe site on its own address, and a preview per branch
cut a releaseforge release publishdownloads with signed checksums
invite someonessh git@src.rest invitea one-time link to join, as a collaborator or reader

The examples use src.rest, where forge hosts itself as cat/forge. cat runs it; fox is another user.

02 browse

Every branch, file, commit and review is a page

URLs are paths: /cat/greet/tree/main/greet.sh/ is that file on main, and every branch has the same pages. Markdown is rendered and code is highlighted. The home page lists people, recent activity and repos by topic.

a repo's pageshttps://src.rest/cat/greet/...
pagewhat is on it
/cat/greet/open reviews, recent commits, the README
tree/<branch>/, blob/<branch>/<path>/files and directories on any branch
log/<branch>/, commit/<sha>/history, each commit with its diff and CI result
reviews/, reviews/<id>/reviews, as stacks, and each review's diffs and discussion
wiki/, sites/, releases/the wiki from wiki/, deployed sites and previews, downloads
insights/pushes, reviews and CI results over time, charted
atom.xmla feed of the newest commits on main
  • Search. / searches a repo's commits, reviews, wiki, paths and file contents; t finds a file on the current branch.
  • The keyboard. j k move through a list, n N through a review's commits, [ ] up and down a stack, v marks a file viewed. ? lists them all.
  • Live. An open page updates itself after a push, without losing your place.
  • Feeds. Every repo has an Atom feed of its newest commits.
  • Diagrams. ```mermaid fences in any markdown file are drawn as diagrams, without JavaScript.
  • Blame and history. A file on main shows who last changed each line, and every commit that changed it with the review it landed in.

03 reviews

Push a branch and it is a review

Any branch other than main is a review once pushed: its commits' diffs in order, plus a combined diff. A branch made from another review's branch stacks on top of it.

$ git switch -c cat/flags
$ forge push -t "parse --name" -d "Flags, parsed by hand"
remote: review #2: https://src.rest/cat/greet/reviews/2/
    1. 6771779README: document flagscat/help
    2. dff8bcaadd --help

    #3 Usage and --helplayer 2, on cat/flags

    1. 80b9ddaadd --shoutcat/flags
    2. afd124bparse --name

    #2 parse --namelayer 1, on main

    1. 895bc48README: be politemain
fig. 1a stack: cat/help was branched from cat/flags
  • Revisions. Each push is a revision. The page shows what changed since the last one, commit by commit, even after a rebase.
  • Stacks. forge push --stack pushes every layer, lowest first; [ and ] move between layers on the page.
  • Status. Open, draft (--draft, or a wip/ branch), merged once it is in main, closed when its branch is deleted.
  • Reading. Files fold, can be marked viewed and stay viewed until they change.
1:  4146e8e ! 1:  afd124b parse --name
    @@ tests.txt
    + ## wiki/Design.md ##
     - Flags are parsed by hand; no getopt.
     - Output goes to stdout, errors to stderr.
    ++- `--name` takes the next argument verbatim.
2:  ff819c4 = 2:  80b9dda add --shout
3:  d70bd83 = 3:  dff8bca add --help
4:  ef1d881 ! 4:  6771779 README: document flags
    -+    --shout       uppercase the greeting
    ++    --shout       UPPERCASE the greeting
fig. 2what changed between two revisions, shortened: = unchanged, ! changed

Comments and approvals

Signed in, you comment and approve at the end of a review page, or comment on a line with the + in the combined diff. From the terminal:

$ forge comment 2 greet.sh:12 -m "Quote the name?"
$ forge approve 2

An approval is for the code as it is: rewording a commit keeps it, changing the code marks it stale. You can't approve your own review. After a new revision, older comments are marked outdated and keep the line they quoted. A repo can require someone else's approval before main moves.

Approval rules

How much approval, and from whom, can depend on what a change touches. .forge/approvals on main gives each path a rule; a change needs every rule its files take. The rules are read from main as it was, so a review can't loosen its own.

# paths                rule
.forge/ bin/forge      2
lib/identity.ts        1 from fox
lib/                   1 + agent
*.md wiki/             none
*                      1

A review page starts with what landing needs: CI, each rule and who has approved, and the latest agent review.

Agent reviews

forge critique runs the reviewer main names, .forge/review, over a review on your machine: Claude Code, Codex or a script of your own. It gets the diff and the project's rules in .forge/review.md. Its findings become comments marked agent, some blocking, and it signs what is still open into a git note, as CI does. The next run looks only at what changed since and carries the open findings forward until the reviewer, or you with --resolve, says they are dealt with. A rule's + agent wants one with nothing blocking.

Landing

forge land checks CI (and approval, if required), fast-forwards main and deletes the landed branches. Over ssh the forge does it itself, no clone needed:

$ forge land 2
$ ssh git@src.rest land cat/greet 2

main only fast-forwards, so rebase a review onto main before landing; git rebase -i --update-refs moves every layer of a stack with it.

04 ci

CI runs on your machine and is signed

forge attest runs .forge/ci on your machine, on a clean checkout of each commit. It signs the result with your SSH key. forge push uploads the results, and every commit, review and branch shows pass or fail and who signed it. Failures are shown too.

forge-attestation 1
type ci
commit ff819c459d6f222c...
tree 69a735fe3cc0a8a8...1
script .forge/ci 99a0d5ea2eee...2
result pass3
exit 0
started 2026-10-18T02:00:14Z
duration 0s
runner cat@laptop4
system Darwin 25.5.0 arm64
env host
log sha256:6cc79019... 4 lines
|ok   greet --name fox
|ok   greet --shout
-----BEGIN SSH SIGNATURE-----5
U1NIU0lHAAAAAQAAADMAAAALc3No...
  1. The files, not the commit. A reworded or reordered commit with the same files keeps its result, and forge attest skips what already passed unless you pass --force.
  2. The script, by hash: change .forge/ci and it is a different claim.
  3. The result and exit code.
  4. The runner as the signer describes it: user, machine and system, plus the Nix lock file if there is one.
  5. The signature over everything above. Log lines start with |, so output can't fake a field.
fig. 3a signed CI result, shortened
$ forge attest --stack           # every commit on top of main
$ forge verify                   # each commit's results, and who signed them

What a verified pass proves

  • Someone with a key on this forge claims these exact files passed this exact script, on a machine they describe.
  • Nobody edited it since: turning result fail into result pass breaks the signature.
  • Who signed it.

What it doesn't

  • That the person is honest. Someone can sign a pass for tests they never ran.
  • That the machine was clean or the setup reproducible, unless Nix pinned it.
  • Anything about commits nobody tested.

To make it stronger, keep the key in hardware: a Secure Enclave key through Secretive, or a FIDO sk- key. Add a flake.nix so CI runs under nix develop. Have a second person run forge attest too; results from several people add up.

A repo can require a verified pass before main moves (for the new tip or every new commit) and for a release's tag. Pushes and lands without one are refused.

CI can also report numbers about a commit, a line each to $FORGE_METRICS: bundle_kb 412 KB, tests 318. They are signed with the result, and the repo's insights chart each one commit by commit on main.

05 sites and releases

Sites and releases ship with the repo

Any repo can publish static sites: docs, a Docusaurus build, a small app. List each in .forge/sites with its output directory and build command, then deploy:

# .forge/sites: name, output, build command (- for none)
docs    build   npm ci && npm run build

$ forge site deploy docs

On main this publishes the site; on any other branch, a preview of that branch, linked from its review. Deleting the branch removes its previews. Every deploy is kept, so you can roll back. This guide is a site of cat/forge.

Each site gets its own hostname, so sites can't read each other's cookies or storage:

where a site is served*.pages.src.rest
deployaddress
cat/greet, site docsgreet-docs.pages.src.rest
fox/dotfiles, site homedotfiles-home-fox.pages.src.rest
docs on the branch cat/flagsgreet-docs--cat-flags.pages.src.rest
cat/forge, site guideforge-guide.pages.src.rest, this page

The forge's owner gets the short names. Sites are served like Cloudflare Pages: _headers and _redirects work, /about finds about.html, a missing page gets the nearest 404.html and previews aren't indexed.

Releases

A release is a tag's files plus a SHA256SUMS signed with your key, bound to the repo and tag. The releases page shows who signed, and anyone can verify a download:

$ forge release publish v1.0 greet.sh greet-1.0.tar.gz
$ forge release verify --url https://src.rest/cat/greet/releases/download/v1.0/ greet.sh
ok greet.sh: matches SHA256SUMS of cat/greet v1.0, signed by cat

Downloads keep stable links: /cat/greet/releases/download/latest/greet.sh is always the newest. A private repo's releases are for its readers only.

06 people

Who can do what

A user is a name and an SSH key; the same key signs their CI results, deploys and releases. An admin invites someone with a link:

$ ssh git@src.rest invite fox --repo cat/012 --as collaborator
https://src.rest/login/#invite=...

The link works once, for a week: fox opens it, pastes a public key and can add a passkey. Each repo's owner decides who else works on it:

$ ssh git@src.rest collaborators cat/012 add fox   # may push anything
$ ssh git@src.rest readers cat/012 add fox         # may read it while it is private
$ ssh git@src.rest who cat/012
owner         cat
collaborator  fox
who can do whaton cat/012
owner, collaboratorsreaders, and any user of a public repoanyone
read pages, cloneyesyesa public repo
comment, approveyesyesno
push a review, a preview, CI resultsyesunder their own name, fox/...no
move main, land, tag, deploy, releaseyesnono
make it private or public, choose readersyesnono
choose collaborators, remove itthe ownernono

Private repos

A private repo is visible only to its owner, collaborators and readers: over ssh, on the web when signed in and over HTTPS with a token. To everyone else it doesn't exist.

$ ssh git@src.rest new --private notes
$ ssh git@src.rest private cat/012     # or public, after a check of its whole history for secrets

Passkeys and tokens

There are no passwords. Passkeys and tokens are made over ssh; lose one and make another.

$ ssh git@src.rest passkey               # a one-time link that adds a passkey
$ ssh git@src.rest token --repo cat/012  # for curl and HTTPS clones, shown once
$ ssh git@src.rest sessions              # where you are signed in; rm signs one out

07 day to day

Day to day

once

git config --global forge.signingkey ~/.ssh/id_ed25519.pub
The key that signs CI results and deploys; a .pub path signs through ssh-agent.
ssh git@src.rest signers > ~/.config/forge/allowed_signers
Then point forge.allowedsigners at it, so forge verify can check signatures.
ssh git@src.rest new greet "say hello"
Make a repo under your name; prints the remote to add. --private makes it private.
git push git@src.rest:cat/greet.git main
Or just push: the repo is created if it doesn't exist.
forge import https://github.com/you/thing.git
Copy a repo in (set forge.host first). --branches takes every branch.

a review

git switch -c cat/topic
Branch under your name; once pushed, it is a review.
forge push -t "Title"
Push the branch and the CI results. --draft, --ready, --target <branch>, -d for a description.
forge push --stack
Push every layer of a stack, lowest first.
forge reviews
Open and draft reviews; forge checkout <id> fetches one.
forge comment [<id>] [path:line] -m "..."
Comment on a review, or on one line of it. Without -m, reads stdin or opens $EDITOR.
forge approve [<id>]
Approve the review as it is now.
forge critique [<id>]
An agent review: run main's .forge/review, post its findings, sign what is open. --full, --fix, --resolve <id>.
forge land [<id>]
Check CI and the approval rules, move main forward, delete the landed branches.
ssh git@src.rest land cat/greet 7
Have the forge land review 7, no clone needed, with the same checks. forge land --server does it from a clone.

ci

forge attest [--stack] [rev...]
Run and sign CI for commits (default HEAD; --stack, everything on top of main). --force reruns what passed.
forge verify [rev...]
Show each commit's results and who signed them.

sites

forge site deploy [name...]
Build and publish from a clean checkout of HEAD: the site on main, a preview elsewhere. --here builds in your work tree.
forge site push <dir> <name>
Publish a directory you built yourself.
forge site url <name> [branch]
Where a site or preview is served.

releases

forge release publish <tag> <file>...
The tag's files with signed checksums, pushed with the tag and your CI results; again to replace them.
forge release verify <file>...
Check downloads against their checksums, and the signature against the forge's keys.
forge release ls, rm <tag>
This repo's releases; delete one.

people, over ssh

ssh git@src.rest help
Lists the commands you can run.
ssh git@src.rest invite fox --repo cat/012 --as collaborator
Admins only: a one-time invite link. invites lists pending ones.
ssh git@src.rest collaborators cat/012 add fox
Or readers; rm takes it back, and no argument lists them.
ssh git@src.rest who cat/012
Its owner, collaborators and readers.
ssh git@src.rest ls
Your repos, and the private ones you may read.
ssh git@src.rest passkey
A link that adds a passkey; token for curl and HTTPS clones.

08 not yet

What isn't there

  • Issues. No bug or task tracker yet.
  • Notifications. No email or push notifications; open pages update themselves.
  • CI on the server. By design: CI runs on your machine, and the forge checks signatures.