JoyJoy Docs

Forges

The forge connector, signing in from the CLI with joy forge, where a token is stored per operating system, and how an operator registers a self-hosted instance.

Not in a joy release yet

The newest joy release is v0.20.0, and it carries no joy-forge binary, no joy forge command group and no pinned host key file. This page describes the release that brings them, so that it is written down before it lands. Until that release is out, cargo install joy-cli and the installers place joy alone, and joy forge is not a command.

joy speaks to GitHub, GitLab, Gitea, Forgejo and Codeberg through a connector: a separate binary called joy-forge that ships in the same release archive as joy. All forge knowledge lives there. joy itself knows no host names, no API shapes and no alias address formats.

There is nothing to configure to get it. cargo install joy-cli builds joy-forge beside joy, and the installers put both in the same directory and name both in the install receipt, which is how joy knows which files it installed. joy update keeps the two in step for a different reason: it runs the release's own installer, and that one copies every binary the archive carries.

One binary, three old names

Before this, each forge had its own binary: joy-github, joy-gitlab and joy-gitea. They still work for the verbs they knew, and a stale copy beside a fresh joy-forge is harmless, because joy-forge answers first. joy forge plugins names any stale copy and prints the rm line for it. joy never deletes a binary it did not install.

Signing in from the CLI

One command group, and every sentence in joy that says "sign in to the forge" points at it:

joy forge login                  # sign in to the host of this project's remote
joy forge login --host github.com
joy forge login --token-stdin    # paste or pipe one token instead
joy forge login --for write      # ask for the rights this level needs
joy forge login --login scotty-work
joy forge status                 # one row per host: login, state, source, scopes, expiry
joy forge logout --host github.com
joy forge logout --all
joy forge plugins                # which connector binary answers, and where it is

Without --host, the host is the one joy really contacts for this project: origin, or the first configured remote.

login runs the forge's own sign in flow and prints the verification URL and the code while it waits. joy opens no browser for you. --token-stdin reads exactly one line from standard input instead, validates it against the forge before storing it, and refuses an empty line. A token is never a command line argument, so no process list can carry it, and there is no --token <value>.

status exits 1 when no host in its set is signed in, and login exits 1 on every result other than a successful sign in. Each of the four commands answers a single {"version":1,"data":{...}} envelope with --json, and keeps every diagnostic on standard error.

logout removes what joy holds. Where the credential came from gh, glab or tea, joy removes nothing and prints that CLI's own command instead, because joy never writes a foreign CLI's store.

The public sign in flows are not live yet

joy's own OAuth applications for github.com, gitlab.com and codeberg.org are not registered yet, so joy forge login on those hosts answers with that fact and names the two ways that do work today: joy forge login --token-stdin, or a client_id for the host in forges.yaml. Nothing is sent to a forge in the meantime.

Where a token comes from

For one call, in this order, and the answer says which step it was:

  1. a variable the caller named explicitly. That is the whole answer for that call: if the variable is empty, the call has no credential and nothing else is consulted;
  2. joy's own entry for that host and login;
  3. the forge's own environment variables (GH_TOKEN, GITLAB_TOKEN, GITEA_TOKEN and the rest);
  4. the forge CLI, by running it.

Which of your logins answers for a repository is decided in one order too: the pin you set for that host in that project, then the login that last pushed to this remote successfully, then the only login the host holds, then one API call per candidate to see which one can reach the repository. If none can, joy says so by name rather than pushing under whichever account happened to be active.

The pin lives in the project's local app state, never in project.yaml, because project.yaml is committed and synced to the forge; a pin there would publish one person's work account to the whole team.

Where joy keeps a token

The connector binary owns the entry and it is the only process that touches it. The app and the CLI both call that same binary instead of reading a store themselves, which on macOS is the only arrangement that avoids a confirmation dialog.

SystemStoreWhat that means for you
WindowsCredential ManagerAny process of your logon session can read it. The entry is written with enterprise persistence, so it roams with a roaming profile.
macOSlogin keychainOnly the binary that created the entry reads it without a prompt.
LinuxSecret ServiceUsed where a session bus and an unlocked collection exist, with persistence switched on so the entry survives a reboot.
Linuxkernel keyutilsAn in-memory cache only. It does not survive a reboot.
Everywhere elsea file joy writesSee below.

Where the operating system's credential store cannot answer, joy writes the token itself, to forge-tokens.json in joy's own configuration directory, with mode 0600 in a directory with mode 0700, and says "source": "file" rather than pretending otherwise. That is the honest case on a headless server, in a container and on any machine whose store refuses. It is the same fallback gh documents for itself: if a credential store is not found or there is a problem using it, the token is written to a plain file.

"Cannot answer" is detected, not guessed: after a write the entry is read back through a fresh handle, and a store that cannot return what was just written did not store it.

joy's configuration directory is $XDG_CONFIG_HOME/joy or ~/.config/joy, plus ~/Library/Application Support/joy on macOS and %APPDATA%\joy on Windows.

Self-hosted instances

A self-hosted forge used to be reachable only when gh, glab or tea was already signed in to it, which makes the sign in door circular for an organisation: nobody can sign in through joy because joy does not claim the host, and joy does not claim the host because nobody signed in.

One file cuts that circle. Put forges.yaml in joy's configuration directory:

- host: git.acme.test
  kind: github            # github | gitlab | gitea
  api_base: https://git.acme.test/api/v3
  web_base: https://git.acme.test
  client_id: Iv1.the-app-registered-on-this-instance
  device_endpoint: https://git.acme.test/login/device/code
  auth_endpoint: https://git.acme.test/login/oauth/authorize
  token_endpoint: https://git.acme.test/login/oauth/access_token
  scopes: repo user:email
  ca_bundle: /etc/pki/tls/certs/acme-root.pem   # Linux only

With it, the instance is claimed and asked with no forge CLI installed at all, and the sign in uses the application your own operator registered. host and kind are the only required keys; everything else defaults to that forge's own rule for the host. The per-project override (joy project set forge gitea) keeps working and wins for that project.

What a connector may do

The connector holds its own credential and has named side effects, so it is not a read-only reporter:

  • it signs you in, stores the credential and signs you out;
  • it answers who you are on that forge, what a repository looks like, and which web address an alias belongs to;
  • it creates a repository and publishes a release, over the forge's REST API and its own HTTPS client. No curl and no gh sits on any API path.

Because the connector speaks HTTP itself, everything on What joy reads from your machine about proxies and certificate stores applies to it as well.