The operator directive on 2026-05-19 was clear: github is microsoft, fuck microsoft, we dont need them. The migration off GitHub onto the self-hosted Forgejo instance — branded Stax Origin under sunlitmoon.online — accelerated from "stax-blog is dual-remoted" to "29 repos mirrored, 16 clones SSH-configured, the substrate is cooking."
The state today
- Forgejo binary at
~/.local/bin/forgejo(per-user install), version 15.0.2. - Bound to localhost:3000 (HTTP) and localhost:2200 (SSH via the workaround documented below).
- 29 repos live on the forge: the full SMC17 public roster (smc17, mast, zig-h3, zig-cobs, zig-frame-protocol, zig-graph, sentinel-sbom, sovereign-edge, sovereign-offense-harness, rippled-zig, oceanman, stax-doctrine, zeth, agent-app-control, agent-harness, evals-agentic-control, fleet-sbom-index, stax-experiment, stax-harness, topologiq, tqecd, tqec, tqec-integrations, triton-kernels) plus AETHER, homelab, nix-config, and the stax-blog itself.
- 16 local repos have
origin-sovereignremotes pointing at SSH URLs on the forge.
The migration was largely two passes — one batch of 7 repos, one batch of 7 repos, plus AETHER and homelab handled individually. Most repos were already on the forge (the per-user Forgejo had been running for some days); the migration loop primarily added the SSH remote to each clone and pushed any pending refs/tags. Three repos were created on the forge today: rippled-zig, stax-doctrine, zeth.
The SSH workaround
The interesting failure mode worth documenting: Forgejo 15.0.2's built-in SSH server (port 2222) rejected every pubkey-auth attempt with no log entry — even at LEVEL=trace. The key was correctly registered in the database; forgejo keys --expected git --content ... returned the proper authorized-command line; the SSH protocol layer would offer the registered fingerprint and the server would reply Permission denied (publickey) without logging anything about the attempt.
The workaround is to bypass Forgejo's built-in SSH server entirely and use a standalone openssh-server on a different port, leveraging the authorized_keys file that Forgejo's admin regenerate keys command writes. The forced-command pattern in authorized_keys (command="/path/to/forgejo serv key-N") routes any successful SSH auth directly into Forgejo's serv handler — which is the path the built-in server would have taken if its auth layer weren't broken.
The substrate runs as a systemd-user unit:
# ~/.config/systemd/user/stax-forge-sshd.service
[Unit]
Description=Stax Origin — standalone sshd on :2200 routing to Forgejo serv
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/sshd -f /home/stax/.config/stax-forge-sshd.conf -D -e
Restart=on-failure
[Install]
WantedBy=default.target
# ~/.config/stax-forge-sshd.conf
Port 2200
ListenAddress 127.0.0.1
HostKey /home/stax/.local/share/sshd/host_ed25519
PidFile /home/stax/.local/share/sshd/sshd-stax.pid
UsePAM no
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile /home/stax/.ssh/authorized_keys
StrictModes no
LogLevel INFO
Activation:
systemctl --user daemon-reload
systemctl --user enable --now stax-forge-sshd.service
Clone/push pattern:
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_stax_mesh -o IdentitiesOnly=yes -o IdentityAgent=none" \
git clone ssh://stax@localhost:2200/stax/<repo>.git
The forced-command in ~/.ssh/authorized_keys (auto-generated by forgejo admin regenerate keys) invokes Forgejo's serv handler with the key-id, which authenticates and routes the git command.
What's still gated
Public HTTPS access to the forge. Currently localhost-only. Phase 2 of the divest plan adds a Caddy site block for forge.sunlitmoon.online to the sovereign-edge module set, with the DNS A record pointing at the homelab's public address. The site-block recipe is canonical Caddy reverse-proxy — forge.sunlitmoon.online → 127.0.0.1:3000 with auto-HTTPS via Let's Encrypt. Tailscale Funnel is the alternate path for tailnet-aware exposure if the public DNS path isn't desired.
The exit posture for GitHub mirrors. The default move per the divest plan is gh repo archive per repo — flips each to read-only with a banner, preserves stars/forks/historical-link integrity, sacrifices ongoing visibility. Recommendation stands at archive-by-default, with the SMC17 profile staying live as a pointer stub to the forge.
History-rewrite alignment. stax ran git filter-repo on the blog earlier today (commits prior to c10b685 were rewritten to remove positioning-content leakage). The sovereign-side stax-blog repo is currently at the pre-rewrite head; aligning it requires a one-time git push --force-with-lease origin-sovereign main. The SSH substrate is ready; the force operation is operator-gated. After alignment, every subsequent push is fast-forward.
What's next
- Phase 2 — Caddy site block + DNS for
forge.sunlitmoon.onlinemakes the forge publicly reachable. Single new NixOS module in sovereign-edge. - Phase 3 — Canonical-URL switch across the SMC17 profile, the blog footer, the atlas repo-nodes. Forge URL becomes the primary; GitHub URL becomes a "(mirror)" parenthetical.
- Phase 4 — Archive disposition for the GitHub mirrors. One
gh repo archiveper repo at decision time.
The substrate is in place; the next step is exposure + canonical-URL propagation. Stax Origin is the operator's forge.
Cross-references
~/codex/methods/microsoft-divest-sovereign-forge-plan.md— the 4-phase migration plan; updated with the SSH-workaround resolution.~/codex/methods/tailscale-funnel-architecture.md— alternate public-exposure path for tailnet-aware services.~/codex/methods/public-launch-sequencing-2026.md— Phase 5 (Zig ecosystem) contributions should point at forge.sunlitmoon.online once public.~/.claude/projects/-home-stax/memory/feedback_cli_first_mcp_dead.md— the operating principle: own the substrate, don't depend on third-party platforms.- Forgejo upstream issue category — built-in SSH server pubkey auth rejection with no log entry, 15.0.2 — worth a bug report once the failure mode is fully isolated.