<title>Hephaestus docs — how to sign in, push, pull, self-host</title>
<metaname="description"content="Hephaestus documentation: wallet sign-in, creating repos, the OCI container registry, personal access tokens, and self-hosting the whole stack from silentmode/hephaestus.">
<pstyle="font-size:1.05rem">A code forge you sign in to with a Bitcoin Cash wallet. Repos on a fast local disk, cold blobs on Sia, container registry baked in. Open source, self-hostable. This page is the map — every section is a one-shot recipe.</p>
<h2id="what">1. What Hephaestus is</h2>
<p>Hephaestus is a <ahref="https://forgejo.org/">Forgejo</a> instance with a Silent Mode auth-proxy in front: instead of email + password, sign-in uses a Bitcoin Cash wallet signature (BIP-137 "Bitcoin Signed Message"). Everything else — git, PRs, issues, releases, container/npm registries, the API — is stock Forgejo. Nothing custom for you to learn if you have used Gitea or Forgejo before.</p>
<p>The same Forgejo is reachable at two addresses. Same account, same repos, same session cookie — differs only in how you got there:</p>
<table>
<tr><th>Host</th><th>How to reach it</th><th>Cert</th></tr>
<tr><td><b>hephaestus.x</b></td><td>A BCNR-aware browser (Theseus, Ariadne) resolves it directly, or through the public gateway at <ahref="https://navigate.st/bns/hephaestus.x/"><code>navigate.st/bns/hephaestus.x/</code></a></td><td>Silent Mode Argonautica CA</td></tr>
<tr><td><b>code.silentmode.st</b></td><td>Regular DNS + Let's Encrypt. Works in any browser, no resolver setup</td><td>Let's Encrypt (public)</td></tr>
</table>
<divclass="note info"><b>Which one to use?</b> If you're setting up an OAuth callback, a docker registry, or a git remote — use <code>code.silentmode.st</code>. It works everywhere. Use <code>hephaestus.x</code> when the whole path is BCNR-aware.</div>
<h2id="signin">3. Sign in with a wallet</h2>
<p>The sign-in button gives you three routes:</p>
<ul>
<li><b>Sign up</b> — creates a fresh BIP-39 wallet in your browser, shows you the recovery phrase, and encrypts the wallet in <code>localStorage</code> with a passphrase you set. <spanclass="pill">easiest</span></li>
<li><b>Sign in</b> — you paste a 12- or 24-word recovery phrase you already have. Same wallet as anywhere else that speaks BIP-39.</li>
<li><b>WizardConnect</b> — sign with an external wallet (Cashonize / Paytaca) via an encrypted Nostr relay. Keys never leave the wallet. <spanclass="pill">most private</span></li>
</ul>
<h3>What happens on the wire</h3>
<ol>
<li>Forgejo redirects you to the auth-proxy's <code>/auth/authorize</code>.</li>
<li>Your browser POSTs <code>{cashaddr, state}</code> to <code>/auth/challenge</code> and gets back a nonce + message.</li>
<li>The wallet signs the message. Standard "Bitcoin Signed Message" (varint-prefixed magic + double-SHA256 + secp256k1 recoverable-compact + base64).</li>
<li>Browser POSTs <code>{nonce, signature, state}</code> to <code>/auth/verify</code>. The auth-proxy recovers the pubkey, re-hashes to a cashaddr, matches, mints an Ed25519-signed <code>id_token</code>, and hands back a Forgejo callback URL.</li>
<li>Following the callback lands you in the dashboard with a session cookie.</li>
</ol>
<h3>If you use the flow programmatically (no browser)</h3>
<p>Every step is a JSON POST. The wallet-signing bit is the only crypto: sign the exact <code>message</code> string the server returned — <b>no trailing newline</b>. See <ahref="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/auth-proxy/src/verify.ts">auth-proxy/src/verify.ts</a> for the byte-perfect reference. A working sample lives in <ahref="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/PROTOCOL.md">PROTOCOL.md</a>.</p>
<h2id="rename">4. Rename your username</h2>
<p>The auto-generated <code>bch_<addr-prefix></code> is a placeholder. Rename yourself to anything else — <code>BitcoinCash</code>, <code>alice</code>, whatever's not taken.</p>
<h4>From the UI</h4>
<p><ahref="https://code.silentmode.st/user/settings">Settings → Profile → Username</a>. Change the field, save. All your repo URLs move to <code>/<new-name>/…</code> and the old URLs 307-redirect for a while so bookmarks survive.</p>
<p>Forgejo names — usernames, org names, repo names — accept letters (mixed case), digits, hyphens, underscores, <b>and dots</b>. So all of these are valid:</p>
<p>Rejected: whitespace, slashes, and reserved routes like <code>assets</code>, <code>login</code>, <code>api</code>. If you're picking a name for an on-chain <code>.x</code> project, the natural mapping (<code>game.x</code> repo owner) works — no need to substitute a hyphen.</p>
<divclass="note"><b>Push over HTTPS</b> uses a Personal Access Token as the password (see §6). Do not paste a wallet passphrase there — it's a Forgejo token, not your wallet.</div>
<h2id="tokens">6. Personal access tokens</h2>
<p><ahref="https://code.silentmode.st/user/settings/applications">Settings → Applications → Generate New Token</a>. Pick the scopes you actually need — the token that pushes commits does not need <code>write:organization</code>.</p>
<table>
<tr><th>Scope</th><th>What it does</th></tr>
<tr><td><code>read:repository</code>, <code>write:repository</code></td><td>Clone / push git over HTTPS, use the repo API</td></tr>
<tr><td><code>read:package</code>, <code>write:package</code></td><td>Docker login + push/pull on the container registry</td></tr>
<tr><td><code>write:organization</code></td><td>Create repos under an org you belong to</td></tr>
<tr><td><code>read:user</code>, <code>write:user</code></td><td>List and revoke your own tokens via API</td></tr>
</table>
<divclass="note warn"><b>Tokens are shown once.</b> Copy the value on the "generated" screen — after you leave the page, only the last 8 characters are shown. Lost tokens are revoked and re-issued, not recovered.</div>
<h2id="registry">7. Container registry (OCI)</h2>
<p>Forgejo speaks the standard OCI Distribution API at <code>code.silentmode.st/v2/</code>. Docker + <code>skopeo</code> + <code>buildah</code> + Kubernetes all work as-is.</p>
<p>Owner is a username or an org you belong to. Owner-org pushes need you to be an <b>Owners</b> team member; add via the admin API or the org settings page.</p>
<p><b>Anonymous pull works for public repos.</b> The <code>/v2/</code> endpoint returns 401 to guide clients through the Bearer-challenge flow, but the token endpoint issues an anonymous token for public content. This means external systems like <ahref="https://home.runonflux.io/">Flux</a> can pull from Hephaestus without any credential setup.</p>
<p>The image is now pullable at <code>code.silentmode.st/<owner>/<app>:latest</code>. If the repo is public, Flux + friends can pull it anonymously (§7 above). Verify it in <ahref="https://code.silentmode.st/-/packages">/-/packages</a> or via the list-packages API.</p>
<tr><td><code>DELETE /api/v1/repos/<o>/<r></code></td><td>Delete a repo (destructive, no undo)</td></tr>
<tr><td><code>POST /api/v1/repos/<o>/<r>/branches</code></td><td>Copy a branch (rename via copy + delete)</td></tr>
</table>
<divclass="note info"><b>Encoding gotcha.</b> Descriptions with UTF-8 characters (em-dashes, curly quotes) need <code>Content-Type: application/json; charset=utf-8</code> AND a properly UTF-8-encoded body. Shell interpolation on Windows Git Bash silently CP1252-encodes and the char lands as <code><EFBFBD></code>. If that happens, PATCH again with a Python <code>urllib</code> caller or plain <code>curl --data-binary @body.json</code>.</div>
<h2id="storage">9. Where files live</h2>
<dlclass="spec">
<dt>git repos</dt><dd>VPS SSD at <code>/data/git/repositories/<owner>/<repo>.git</code> inside the Forgejo container</dd>
<dt>Postgres</dt><dd>SSD at <code>/var/lib/postgresql/data</code> — issues, PRs, users, sessions, tokens</dd>
<dt>LFS objects</dt><dd>Sia via S3 at <code>s3.silentmode.st:8600</code>, bucket <code>hephaestus-lfs</code></dd>
<dt>Release attachments, packages</dt><dd>Same Sia bucket, different key prefixes</dd>
<dt>Container images</dt><dd>Same Sia bucket, prefix <code>packages/container/</code></dd>
<dt>Nightly backup</dt><dd><code>restic</code> encrypts the whole tree + Postgres dump to a separate Sia bucket, retained 30 days</dd>
<dt>DR mirror</dt><dd>Static landing (this page) also mirrored to Sia bucket <code>bns/hephaestus.x/</code>, updated on redeploy</dd>
</dl>
<h2id="selfhost">10. Self-hosting</h2>
<p>You can run your own Hephaestus on any Linux box with Docker. Nothing about it depends on Silent Mode's infrastructure — the operator's role is just to own the domain and hold the wallet secrets.</p>
<spanclass="cm"># 2. configure — copy .env.example, fill in your domain, Sia S3 creds, OIDC secret</span>
cp .env.example .env
$EDITOR .env
<spanclass="cm"># 3. run</span>
docker compose up -d
<spanclass="cm"># 4. first wallet signs in and takes the admin seat</span>
open https://your-domain.example/user/oauth2/hephaestus-wallet</pre>
<p>Full deploy notes: <ahref="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/README.md">README</a>, and <ahref="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/PROTOCOL.md">PROTOCOL.md</a> for the wire format.</p>
<li><b>Push over HTTPS asks for a password.</b> That password is a <b>Personal Access Token</b>, not your wallet passphrase. Wallet passphrases decrypt the wallet in your browser's <code>localStorage</code>; they have nothing to do with Forgejo's git auth. See <ahref="#repos">§5</a> for the git-remote URL and <ahref="#tokens">§6</a> for how to generate the PAT.</li>
<li><b>Descriptions show as <code><EFBFBD></code></b> when shell → curl → API re-encodes UTF-8 as CP1252 (Windows Git Bash is the usual culprit). Use a Python <code>urllib</code> PATCH or <code>curl --data-binary @file</code>. Full recipe with the correct <code>Content-Type</code> header is in <ahref="#api">§8 → "Encoding gotcha"</a>.</li>
<li><b>Signature mismatch on <code>/auth/verify</code></b> almost always means your message string got a trailing newline (Python <code>print</code>, Bash heredoc). Write the message to a file with <code>newline=''</code> and hash the file bytes directly.</li>
<li><b>OIDC callback URL uses port 3000</b> if Forgejo's <code>ROOT_URL</code> is unset. Setting <code>[server].ROOT_URL = https://code.silentmode.st/</code> in <code>app.ini</code> fixes it; browsers can't reach internal port 3000.</li>
<li><b>Cross-origin session cookies.</b> Starting OAuth on hephaestus.x and finishing on code.silentmode.st loses the session cookie mid-flow. The landing dropdown always kicks off on code.silentmode.st to avoid this.</li>
<li><b>Renaming a user via admin API</b> takes <code>new_name</code> as a form field, not JSON. JSON returns 422 "NewName required". Full command in <ahref="#rename">§4 → "From the admin API"</a>.</li>
<li><b>SSH port is 2222, not 22</b>, so the compose stack doesn't fight the host's sshd. Your git remote URL must include <code>:2222</code>.</li>
<li><b>Docker Hub rate limits</b> can bite <code>docker compose build</code> if you're unauthenticated. Log in once with <code>docker login</code> against Docker Hub before running the stack build.</li>
<h2id="sessions">12. Working with other sessions</h2>
<p>Any tool, colleague, or parallel Claude session that needs to push to Hephaestus goes through the same short workflow. Once you internalize the wallet-vs-PAT split, everything else is one API call at a time.</p>
<h3>Wallet vs Personal Access Token — when to use each</h3>
<p>The wallet is for <b>bootstrapping the account</b> once, and for <b>recovering</b> if a PAT ever leaks. Nothing else. Every git operation, every API call, every docker push, from every session, from every day after the first — runs on a PAT.</p>
<table>
<tr><th>Task</th><th>Auth</th></tr>
<tr><td>Create the user account for the first time</td><td>Wallet sign-in</td></tr>
<tr><td>Docker push / pull to the OCI registry</td><td>PAT (<code>write:package</code>)</td></tr>
<tr><td>Create repos under an org, manage members</td><td>PAT (<code>write:organization</code>)</td></tr>
<tr><td>Revoke a compromised PAT</td><td>Any PAT with <code>write:user</code>, or admin</td></tr>
<tr><td>Rotate ownership (transfer everything to a new wallet)</td><td>Wallet sign-in on the new address + admin transfer</td></tr>
</table>
<divclass="note info"><b>Never share a wallet mnemonic across sessions.</b> A parallel session doesn't need it. Give them a PAT with the narrowest scope that works, and revoke it independently when they're done.</div>
<h3>Bootstrapping a new namespace for another session</h3>
<p>Say a parallel session needs to push code to <code>game.x/checkers</code>. Full recipe, five steps, no browser required for the human at all:</p>
<pre><spanclass="cm"># 1. Admin creates the org (one API call, no wallet involvement)</span>
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
<p>If the wallet owner wants to bootstrap without ever opening a browser — for a headless workstation, a CI job, or an AI session that reads its wallet from disk — the whole sign-in flow is scriptable. The mnemonic sits in a local <code>wallets.json</code>, never gets pasted anywhere, never enters a chat log.</p>
body: JSON.stringify({ nonce, signature, state }) })).json();
<spanclass="cm">// 5. follow callback with a cookie-jar client → session cookie in hand
// → GET /user/settings/applications to mint a PAT for future use</span></pre>
<p>Reference implementations in the tree: <ahref="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/auth-proxy/src/verify.ts"><code>auth-proxy/src/verify.ts</code></a> for byte-perfect signing, and <ahref="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/PROTOCOL.md"><code>PROTOCOL.md</code></a> for the full wire format. Adapt for <code>bitcoincash:</code> / <code>bchreg:</code> by swapping the <code>prefix</code>.</p>
<p><b>Rotation habit:</b> issue one PAT per session, scope narrowly, revoke the moment the session ends or a machine changes hands. Every token has a name — use it (<code>ci-fly-x-deploy</code>, <code>session-2026-09-20</code>) so you know which one to revoke when things move.</p>