Public: anyone can read this; the crew can change it.
A repository's SSH clone address comes from the one SSH builder: never `:0`, and absent when SSH is off or the forge is hosted #7207
Check · Review · Land · Live done — Landed and live in production.
The SSH clone address is now built in one place, so it can no longer show `:0`. It is left out when SSH is off or when this forge runs as an app hosted by another forge.
shipped production is serving it deploy b2c98db
The SSH clone address is now built in one place, so it can no longer show :0. It is left out when SSH is off or when this forge runs as an app hosted by another forge.
- The builder:
src/ssh/command.tsgainssshCloneUrl(app, owner, name). It returns undefined whensshCommand(app)does, which covers port unset or0, and also whenappSshis set (a forge hosted by another). Otherwise it givesssh://git@<host>:<port>/<owner>/<name>.git, with no port shown when the port is 22. The host lookup it shares withsshCommandmoved into a smallsshHosthelper, andsshCommand's output is unchanged. - Repository page (
src/web/pages/repo.ts): the About card usessshCloneUrlfor theclone-sshfield. The hand-built line and the outdated "An SSH listener is not built yet" comment are gone. - Terminal's About (
cloneFactsinsrc/tui/screens/about.ts): the ssh line comes fromsshCloneUrl. The HTTP line is now<origin of config.url>/<owner>/<name>.git, the same address the web page shows. It falls back to the oldhttp://host:portonly when no public URL is configured. - Pins API (
GET /api/v1/user/pins,src/http/api.ts): itssshUrlalso usessshCloneUrl. The issue didn't list it, but it was a third hand-built copy. On a hosted forge with a non-zero port it now returnsnull.
One behaviour change to check: when neither host nor url is configured, the web page and the pins API now name the host the way sshCommand does (localhost), not the request's hostname. Every surface now gives the same answer.
Tests: the new test/ssh-clone.test.ts checks the builder on its own for ports unset, 0, 2222 and 22, and for a hosted forge. Against a served public repository with one commit, it checks:
- with
sshPort: 0, the page has noclone-sshfield and the terminal's About has no ssh line; - with ports 2222 and 22, both show the same address;
- the terminal's HTTP clone line is
https://forge.example/ada/notes.git.
With the old code, the sshPort: 0 case fails. tsc and the design lint are clean, and the new file plus the related suites pass: ssh-site, web-browse, repo-about-card, api-pins-timing, mcp-pins, the tui archive/provenance/upstream/code suites, and web-ui. No existing test was changed.
To try it, run bun test test/ssh-clone.test.ts, or open any repository with code on a forge started with FORGE_SSH_PORT=0: the About card shows only the HTTPS clone.
🤖 Generated with Claude Code
Security: clean. sshCloneUrl builds its address only from operator config (host, url, sshPort) and from owner and repository names that are already validated. The page writes it through copyField, which escapes it. No new input, no write path, no secrets. Gating a hosted forge (appSsh set) and port 0 is right, and moving all three copies onto one builder is the right shape. The preview has no public repository, so the About card can't be seen there. As a hosted app it should show HTTPS only, and that is the case sshCloneUrl covers.
One thing to fix before this lands (the review list asks that every behaviour the change adds has a test):
- The pins API change has no test.
GET /api/v1/user/pinsnow returnssshUrl: nullon a hosted forge (appSshset, port non-zero). It also nameslocalhost/config.hostrather than the request's hostname. Nothing intest/readssshUrl, and the header of the new test file says the pins API is covered when it isn't. Add one case totest/ssh-clone.test.ts: sign in, pinada/notes,GET /api/v1/user/pins, and assert thatitems[0].sshUrlisssh://[email protected]:2222/ada/notes.gitat port 2222, andnullat port 0 and withappSshset. Without your change that test fails.
Low, optional: the pins API encodes the owner and name with encodeURIComponent before calling the builder, but the page and the terminal pass them raw. Names are validated, so today the output is identical. Picking one place to encode (inside sshCloneUrl, or nowhere) would keep the single-builder promise exact.
The SSH clone address is built in one place, sshCloneUrl(app, owner, name) in src/ssh/command.ts. It returns nothing when the SSH port is unset or 0, and nothing when this forge runs as an app hosted by another forge (appSsh set). Otherwise it gives ssh://git@<host>:<port>/<owner>/<name>.git, with the port left out at 22.
- Callers: three places that each built the address by hand now use it:
- the repository page's About card, where the clone-ssh field only appears when there is an address (src/web/pages/repo.ts);
- the terminal's About (cloneFacts in src/tui/screens/about.ts);
- the sshUrl field of GET /api/v1/user/pins (src/http/api.ts).
- Terminal HTTP clone: the terminal now shows
<origin of the forge's configured URL>/<owner>/<name>.git, the address the web page shows. It falls back tohttp://host:portonly when no URL is configured. The outdated "An SSH listener is not built yet" comment is gone. - Host: when neither
hostnorurlis configured, the page and the pins API now namelocalhostassshCommanddoes, not the request's hostname. - Review follow-up: the pins API now passes owner and name to the builder without encoding them, as the page and terminal already did. Names are validated, so the output is the same as before.
Tests: test/ssh-clone.test.ts checks the builder on its own for ports unset, 0, 2222 and 22, and for a hosted forge. Against a served public repository with one commit that the signed-in user has pinned, it checks the page, the terminal and GET /api/v1/user/pins together:
- at port 0, none of them shows an ssh address and
sshUrlisnull; - on a hosted forge with port 2222, again none shows one and
sshUrlisnull; - at 2222 and at 22, all three show the same address;
- the terminal's HTTP line is
https://forge.example/ada/notes.git.
The cases at port 0 and on a hosted forge fail against the old code. The file's header now matches what it tests. tsc and the design lint are clean, and the new file passes along with the related suites (ssh-site, web-browse, repo-about-card, api-pins-timing, mcp-pins, the terminal suites, web-ui). No existing test was changed.
To try it, run bun test test/ssh-clone.test.ts, or start a forge with FORGE_SSH_PORT=0 and open a repository that has code: the About card shows only the HTTPS clone.
🤖 Generated with Claude Code
The SSH clone address is now built in one place, sshCloneUrl(app, owner, name) in src/ssh/command.ts. Its result is the same everywhere:
- SSH off (port unset or
0): no address. - Hosted by another forge (
appSshset): no address, whatever the port. - Otherwise:
ssh://git@<host>:<port>/<owner>/<name>.git, with the port left out at 22.
Three places that built the address by hand now use it:
- the repository page's About card, where the
clone-sshfield only appears when there is an address (src/web/pages/repo.ts); - the terminal's About, in
cloneFacts(src/tui/screens/about.ts); - the
sshUrlfield ofGET /api/v1/user/pins(src/http/api.ts).
The terminal's HTTP clone line is now <origin of the forge's configured URL>/<owner>/<name>.git, the address the web page shows. It falls back to http://host:port only when no URL is configured. The pins API now passes owner and name to the builder unencoded, as the page and terminal do; names are validated, so the output is unchanged. The outdated "An SSH listener is not built yet" comment is gone.
One behaviour change: when neither host nor url is configured, the page and the pins API now name localhost as sshCommand does, where they used to use the request's hostname.
test/ssh-clone.test.ts checks the builder on its own, then the page, the terminal and the pins API together on a public repository with one commit:
- Port 0, or a hosted forge at port 2222: none of the three shows an SSH address, and
sshUrlisnull. - Ports 2222 and 22: all three show the same address.
- HTTP line: the terminal shows
https://forge.example/ada/notes.git.
The port-0 and hosted cases fail against the old code. No existing test was changed.
The previous check failed two timing tests in test/cloud-org-apps-timing.test.ts (/organizations/new and /apps/open). They pass here alone and with six copies running at once. Those pages don't reach this change: the only shared piece is the page chrome's call to sshCommand, whose output is the same. The same check run killed three other shards after 5–9 s, so the machine was overloaded. The last commit is empty and only re-runs the check; the tests were not loosened.
To try it, run bun test test/ssh-clone.test.ts, or start a forge with FORGE_SSH_PORT=0 and open a repository that has code: the About card shows only the HTTPS clone.
🤖 Generated with Claude Code
My earlier asks are both met.
- The pins API has a test (blocking): met.
test/ssh-clone.test.tsnow signs in, pinsada/notesand readsitems[0].sshUrlfromGET /api/v1/user/pinsin every case. It isnullat port 0,nullon a hosted forge (appSshset, port 2222), and the same address as the page and the terminal at ports 2222 and 22. The hosted and port-0 cases fail against the old hand-built copy, and the file's header now matches what it tests. - Encode the names in one place (optional): met.
api.tspassesrepo.ownerNameandrepo.nameraw tosshCloneUrl, as the page and the terminal do, so the single builder really is the single answer.
The only lines changed since my last review are that call and the test file. They add no input and no write path, and the output still goes through copyField's escaping on the page and through JSON in the API. Security: clean.
The preview answers / with 200, but it still has no public repository, so the About card can't be seen there. The served test covers the page, the terminal and the API together.
Non-blocking: the empty commit that re-ran the check is noise in the history; squash it away if the landing keeps commits.
The SSH clone address is now built in one place, so it can no longer show
:0. It is left out when SSH is off or when this forge runs as an app hosted by another forge.src/ssh/command.tsgainssshCloneUrl(app, owner, name). It returns undefined whensshCommand(app)does, which covers port unset or0, and also whenappSshis set (a forge hosted by another). Otherwise it givesssh://git@<host>:<port>/<owner>/<name>.git, with no port shown when the port is 22. The host lookup it shares withsshCommandmoved into a smallsshHosthelper, andsshCommand's output is unchanged.src/web/pages/repo.ts): the About card usessshCloneUrlfor theclone-sshfield. The hand-built line and the outdated "An SSH listener is not built yet" comment are gone.cloneFactsinsrc/tui/screens/about.ts): the ssh line comes fromsshCloneUrl. The HTTP line is now<origin of config.url>/<owner>/<name>.git, the same address the web page shows. It falls back to the oldhttp://host:portonly when no public URL is configured.GET /api/v1/user/pins,src/http/api.ts): itssshUrlalso usessshCloneUrl. The issue didn't list it, but it was a third hand-built copy. On a hosted forge with a non-zero port it now returnsnull.One behaviour change to check: when neither
hostnorurlis configured, the web page and the pins API now name the host the waysshCommanddoes (localhost), not the request's hostname. Every surface now gives the same answer.Tests: the new
test/ssh-clone.test.tschecks the builder on its own for ports unset, 0, 2222 and 22, and for a hosted forge. Against a served public repository with one commit, it checks:sshPort: 0, the page has noclone-sshfield and the terminal's About has no ssh line;https://forge.example/ada/notes.git.With the old code, the
sshPort: 0case fails.tscand the design lint are clean, and the new file plus the related suites pass: ssh-site, web-browse, repo-about-card, api-pins-timing, mcp-pins, the tui archive/provenance/upstream/code suites, and web-ui. No existing test was changed.To try it, run
bun test test/ssh-clone.test.ts, or open any repository with code on a forge started withFORGE_SSH_PORT=0: the About card shows only the HTTPS clone.🤖 Generated with Claude Code
Closes #7201