Skip to main content
Mirage’s built-in gh implementation uses the vocabulary of the official cli/cli project. A repository is a tree, so mirage already reads one as files: the github mount is the read half, and ls, cat and grep are how an agent explores it. gh is the write half, plus the account-level operations a filesystem has no shape for. Licenses: The upstream cli/cli project uses the MIT License; Mirage’s independent implementation uses Apache-2.0.

Install

In YAML the same install rides the clis: section; see the CLI overview. An Enterprise Server base ends in /api/v3, and GraphQL (gh api graphql and the verbs that query it, such as repo view --json) then goes to /api/graphql on the same host, outside the REST base, as real gh sends it. Any other base_url answers GraphQL at base_url/graphql. repo and branch are what real gh reads off the current directory’s git remote and checkout. A workspace has neither, so the install carries them: repo answers a line that names no repository, and both feed the {owner}/{repo}/{branch} placeholders. Two installs under different head words are two accounts.

Authentication status

gh auth status checks the configured token with GET /user, reports the host and account, and exits 1 when GitHub rejects the credential. It needs no mount. The token source is reported as Mirage configuration: configuration retains the resolved secret, not the environment variable or file that supplied it. The credential itself is never printed.

Reading is the mount, acting is the CLI

A write through gh lands on the same repository the mount reads, but it lands by repository name rather than by any vfs path, so the mount has nothing to aim a per-path invalidation at. The executor expires every mount’s caches after a write verb of an account CLI, so the next cat or ls refetches instead of serving the pre-write bytes. Nothing is required of the caller, and nothing on the spec names the mount.

Verbs

Every level answers --help; man gh, man gh issue, and man gh pr create render the same text from the same spec. list also accepts ls, and issue and pull request create also accept new.

Typed workflows

Typed read commands accept -R/--repo [HOST/]OWNER/REPO where applicable. Most also accept --json field1,field2 and -q/--jq EXPRESSION; --jq requires --json, so the selected field set stays explicit and bounded. List commands use -L/--limit and fetch as many pages as needed. gh issue view, gh issue list, gh pr view and gh pr list accept every --json field gh 2.85 does, and gh repo view and gh repo list every repository field. Like gh, they ask GraphQL for exactly the fields named and print each in gh’s own shape, so files, commits, reviews, statusCheckRollup and closedByPullRequestsReferences need no fallback to gh api. pr view reads reviews, comments, closing issues and checks past their first 100, and issue view its comments and closing pull requests. issue view answers a pull request’s number too, as gh does, with the fields only an issue has at their zero. The JSON prints compact, one value per line, the way gh prints it to anything but a terminal.
Mutations never open an editor or prompt. Supply the values on the line, through a workspace file, or through stdin with -:
Body and notes files are read through the workspace dispatcher, not the host filesystem. That keeps an agent inside mirage while still supporting the official CLI’s file-oriented grammar.

repo

The default view prints a name: line, a description: line, then -- and the README, with the separator omitted when the repository has none. Use --json for any of gh’s repository fields or gh api for the complete REST object. rename takes the new name as the operand and the repository to rename on -R, which is the reverse of what the shape of the line suggests; that is upstream’s grammar, not a mirage choice. [HOST/]OWNER/REPO is parsed from the right, so the owner and the repository are the last two segments and a leading host is dropped. The host is accepted for compatibility with lines copied from real gh but does not route: every call goes to the install’s base_url. A second host means a second install.

api

gh api reaches every endpoint that has no typed verb, which is most of them.
{owner}, {repo} and {branch} expand from the install, the way real gh expands them from the current repository. Any other brace pair is left exactly as typed and reaches the wire, which is gh’s behavior too. Quote the endpoint so the shell does not eat the braces. The rules are gh’s own:
  • The method is GET with no fields and POST once a field is given, unless -X says otherwise.
  • A GET carries its fields in the query string; every other method carries them in a JSON body.
  • -f/--raw-field is always a string. -F/--field reads true, false, null and integers as their JSON types. A typed value beginning with @ reads that workspace file, and @- reads stdin.
  • Object keys such as config[enabled] and arrays such as labels[] build nested JSON rather than flat keys.
  • --input FILE sends that JSON document as the body and moves any fields to the query string. --input - reads stdin.
  • -H/--header adds or replaces request headers.
  • --paginate follows REST Link headers, and pages that are arrays print as one array, joined the way gh joins them; --slurp wraps every page in one array instead.
  • --silent suppresses response output without suppressing the request.
  • A call with no fields sends no body at all, so a bare DELETE is a bare DELETE rather than an empty JSON object with a content type. Some endpoints read those differently.
  • The leading slash is optional.
  • A placeholder expands in an endpoint and in a -F value, but not in a -f one, which is the split gh’s own --help describes.
  • A read (GET, HEAD, OPTIONS) leaves the mount’s cache alone; only a write expires it.
A response prints as the server sent it, which is how gh prints it to anything but a terminal: GitHub’s JSON arrives compact, with no newline at the end. -q/--jq evaluates in-process and prints each output as gh does: a string raw, null as an empty line, a number in fixed notation with two decimals unless it is whole (1.50, 3), and anything else as compact JSON with its keys sorted and <, > and & escaped. The rest of the shell can consume stdout just as well:
A --jq program that fails ends the command with exit 1, after the lines it printed first. The report follows gh’s gojq where mirage’s jq can tell what gojq would say: an error the program raises with error(v) reads error: <v>, with v as it is when it is a string and as compact JSON with sorted keys otherwise; halt_error reads halt error: <v>; and halt, like halt_error on null, only ends the output. As with every CLI failure, mirage puts the command’s name first (gh api: error: boom), where gh prints error: boom alone.

Divergences from upstream gh

gh is virtualized, not wrapped. The intentional boundaries are: