# Use the CLI

Manage routing settings, accounts, and connected machines from your terminal.

## Get the CLI

The CLI manages Mirage settings on macOS and Linux. Commands beginning with
`mirage host` manage the host service on the Mac where you run them; `mirage remote` manages
your account through the hosted service.

The [Mac bootstrap installation](https://mirageci.com/docs/local-mac.md#install-and-pair) installs the CLI as
`/usr/local/bin/mirage`. It updates with the host service, including when the menu-bar app is
closed. If that directory is not on your PATH, use it in your current terminal with:

macOS · use the bundled CLI:

```bash
alias mirage='/usr/local/bin/mirage'
mirage --version
```

The alias lasts for this shell session. In scripts, use `/usr/local/bin/mirage`
directly when it is not on your PATH.

Standalone x64 and ARM64 archives for Linux are distributed through Mirage’s
GitHub releases under `cli-v` tags. On macOS, use the CLI installed by the Mac bootstrap
command above; it ships with the app and host service.

## Sign in

Terminal · authorize account access:

```bash
mirage auth login
mirage auth status
```

The login command opens Mirage in your browser. Check that the displayed code matches the
terminal and approve access with your GitHub-linked Mirage account. Use `mirage auth login
--no-open` to open the page yourself, or `--read-only` to request read-only settings access.

The default server is `https://mirageci.com`. Login credentials are stored in your operating
system’s credential store. On an SSH-only shell, the CLI uses a private credential file if
that store is unavailable. Run `mirage auth logout` to revoke the stored login and remove it
from this computer.

Browser sign-in authorizes remote settings. Pairing a Mac separately authorizes that machine
to run jobs.

## Manage this Mac

These commands require the installed Mac host service. They do not require a remote CLI
login.

Terminal · inspect the local host:

```bash
mirage host status
mirage host doctor
mirage host diagnostics --lines 80
```

`status` reports the host’s current state. `doctor` checks prerequisites and runner profiles
and includes the reason an unavailable profile can’t run; the machine page shows the next
step. `diagnostics` returns the host service’s redacted log tail.

Use `mirage host configure --help` for pause and battery settings. Setup also uses it to
choose the first runner settings; after that, change them on the machine page. See
[Set up a Mac](https://mirageci.com/docs/local-mac.md) for the installation and examples.

## Manage account settings

Terminal · inspect your accounts:

```bash
mirage remote get
mirage remote audit
```

Private repositories selected for the GitHub App use local Macs automatically. Use a
returned repository ID only when changing its optional Cloudflare routing:

Terminal · enable Cloudflare for a repository:

```bash
mirage remote repository 67890 --cloudflare true
```

Remote changes follow your GitHub organization role: organization owners administer the
Mirage account, and organization members can read it and offer machines. Manage people on
GitHub. Use `mirage remote --help` for machine access, scheduling, transfers, and Cloudflare
settings.

Owners can also create, connect to, stop, and resume retained macOS VMs through
`mirage vm`. Follow [Agent VMs on a Mac](https://mirageci.com/docs/agent-vms.md) for the commands.

To delete a machine from the fleet, use its ID from `mirage remote get`:

Terminal · delete a machine:

```bash
mirage remote delete-machine 11111111-2222-4333-8444-555555555555
```

The machine's personal controller or an organization owner may delete it. Delete
its retained Agent VM disks first and wait for active work on a connected machine
to finish. Deletion revokes pairing and sharing access; jobs on an unreachable
machine may be interrupted. Past run history is kept. The host software stays installed.

## Use in scripts

Successful commands write one versioned JSON value to standard output and exit with status
zero. Prompts and JSON errors go to standard error; failures exit nonzero. Inspect the
response version when building an integration, and use a JSON parser such as `jq` instead of
matching display text.

Shell script · list private repositories (requires jq):

```bash
mirage remote get | jq '.accounts[].installations[].repositories[] | select(.private == true)'
```

Interactive login uses Keychain on macOS or Secret Service on Linux when available. If it
fails in an SSH-only shell, Mirage automatically
stores the refresh token in an owner-only file under `~/.config/mirage/credentials/`.
Use `mirage auth login --no-open --credential-file` to select that file explicitly in
another headless environment. `mirage auth logout` revokes and removes the file.
For headless `mirage remote` commands, `MIRAGE_ACCESS_TOKEN` can instead hold a dedicated
API token or an existing short-lived OAuth token. The CLI does not store or refresh that
environment token; unset it to stop using it.

## Use the HTTP API

Create a dedicated API token in **Settings → Account → API tokens**. Read-only access is the
default; choose read/write only when an integration needs to change remote settings. Give the
token a name and a 30, 90, or 365 day expiry, then copy it when Mirage shows it once. Store it
in your agent's secret store. Tokens use your current account access, which follows your GitHub
organization role; revoke a token from the same page when it is no longer needed.

These endpoints use `Authorization: Bearer` and return versioned JSON on success. Discover the
installation ID from settings, then read status for that installation:

Shell script · inspect Mirage without installing the CLI:

```bash
curl -fsS -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  https://mirageci.com/v1/settings
curl -fsS -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  'https://mirageci.com/v1/status?installationId=12345'
curl -fsS -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  'https://mirageci.com/v1/status/jobs?installationId=12345&state=active'
curl -fsS -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  'https://mirageci.com/v1/status/runs/987654321?installationId=12345'
```

The `/v1/status` snapshot includes `generatedAt`, queued and running counts, failures in the
last 24 hours, machine connection and capacity, profile availability, and Cloudflare mode and
health. `upstream.github` reports cached health for GitHub Actions, API Requests and Webhooks,
including `state`, `freshness`, component states, `lastSuccessfulFetchAt`, and official incident
links. Unknown means Mirage has no fresh, successful status check; it does not mean GitHub is
healthy. Mirage checks the public status feed once per minute and considers checks stale after
three minutes. `sourceUpdatedAt` is GitHub's last status transition, not the fetch time.
It requires `runners:read`; users without `runs:read` receive `jobs: null`. Job lists
and run lookup require `runs:read`; they return jobs with GitHub IDs and URLs, provider or
failure reasons, and observation times. Run lookup also returns attempts. Each job's
`upstreamContext` lists retained incidents that overlapped its queue or startup window, with
`relationship: "possibly_related"`. An overlap does not establish the cause of a job failure
and never replaces its recorded status or reason. These endpoints read Mirage's cache and
incident history without making requests to GitHub Status.

For job lists, `state` accepts `active`, `failed` (all recorded failures), or `recent`
(updated in the last 24 hours).
Job and run responses return `nextCursor` for the next page. Pass it as `cursor`; `limit`
defaults to 20 and cannot exceed 100. Job pages use descending GitHub job IDs, and the
`recent` cutoff stays fixed across pages. Jobs can change state between requests, so start
a new page sequence for a fresh view. Timestamps are Unix milliseconds. Mirage reports what it
has observed; GitHub remains authoritative for check results. Machine `activeSlots` includes
occupancy from other accounts on a shared Mac. Status responses use `Cache-Control: no-store`;
poll every 30 seconds or less often.

A read/write token can call the existing remote settings endpoints, including Mac host policy
and macOS image settings when its owner has permission. This example disables a repository's
optional Cloudflare route:

Shell script · change a remote setting:

```bash
curl -fsS -X PATCH \
  -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"cloudflareEnabled":false}' \
  https://mirageci.com/v1/settings/repositories/67890
```

The same endpoint accepts `diskExhaustionAction`, `report` or `fallback`: what Mirage does
when one of the repository's jobs [runs out of disk](https://mirageci.com/docs/troubleshooting.md#job-ran-out-of-disk).
Settings list it for each repository.

Job allocations are available with the same token. List the selected installation's jobs to
find the stable `definitionId`, then send the complete CPU, memory, and disk request:

Shell script · read and update a job allocation:

```bash
curl -fsS -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  'https://mirageci.com/v1/settings/job-allocations?installationId=12345'
curl -fsS -X POST \
  -H "Authorization: Bearer $MIRAGE_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"definitionId":"11111111-2222-4333-8444-555555555555","resources":{"kind":"linux","cpuCount":2,"memoryBytes":8589934592,"rootDiskBytes":17179869184}}' \
  https://mirageci.com/v1/settings/job-allocations
```

For a job that already ran on a direct Mirage label but has no registered definition, send
`workflowJobId` instead of `definitionId`. Use the numeric job ID from its GitHub Actions URL.
Mirage reads the observed runner profile and stored workflow plan, and requires one static,
unambiguous job name. This registers only an allocation; it does not change the workflow,
create provider routes, or enable experiments. The response returns its new `definitionId`.
Dynamic job names and matrix children need a registered definition instead.

Reading requires `settings:read`, and updating requires `settings:write`. Both also require
repository-management access to that GitHub account. Set `resources` to `null` to restore
the profile default. Linux requests use `rootDiskBytes`; macOS requests use `writableDiskBytes`.
Changes apply to future capacity leases, so rerun a completed job after updating its request.
These routes support dedicated API tokens and CLI OAuth tokens, and don't require browser
cookies or an `Origin` header. `POST /v1/dashboard/job-allocation` also accepts a bearer token;
its cookie-authenticated browser requests still require the same origin.

Tokens cannot manage other tokens or control interactive VMs. Commands that act on the local
Mac still run on that host. Invalid query values return `400`; a missing or invalid token
returns `401`; insufficient token scope or GitHub role returns `403`; an installation or run
outside your access returns `404`.

## Operational diagnostics

Read the selected GitHub account's queue and lifecycle stages after `mirage auth login`:

Shell script · inspect lifecycle diagnostics:

```bash
mirage diagnostics lifecycle --account ahoylabs
mirage diagnostics lifecycle --account ahoylabs --window-hours 48
mirage diagnostics lifecycle --account ahoylabs | jq '.layers[] | {label, steps}'
mirage diagnostics lifecycle --account ahoylabs | jq '.waiting[] | {job, code, reason}'
```

`--account` is required; `remote get` lists accessible accounts. The reporting
window defaults to 24 hours and accepts 1–168 hours. A read-only login is sufficient.
The command uses `GET /v1/diagnostics/lifecycle?account=ahoylabs&windowHours=24`
with `settings:read` and the account's `runs:read` capability. Each successful read
creates an account-scoped CLI audit event.

The version 1 JSON includes `accountLogin`, `installationId`, `generatedAt`,
`windowHours`, `metrics`, `layers`, raw `counts` and `timings`, the oldest `waiting`
jobs with stable reason codes and display text, `waitingTotal`, `blocked` labels, `sampling` limits,
`capacity.available` and `warnings`.
Intake, Capacity, Runner, Cloudflare and Outcome count their own records, so a
job can appear in several layers. In-flight jobs count Intake only. Terminal
counts cover the reporting window; active counts describe now. Timings sample
up to 500 recent records per layer: active stages measure elapsed age and
terminal stages measure time to finish. P50 is the median and P95 the slow end;
missing samples remain null. The waiting list is limited to 12 jobs; its total
excludes unsupported labels, which are reported separately in `blocked`.

Use `waiting[].code` in scripts; `reason` is display text and may change. The
codes are `cloudflare`, `unsupported_label`, `no_ready_machine`, `slots_busy`,
`awaiting_github`, `leasing_slot`, `awaiting_approval` and `capacity_unavailable`.
Approval and unsupported-label codes remain explicit when capacity is unknown.

If live capacity cannot be read, the command still returns stored counts and
timings. `capacity.available` is false and `warnings` includes
`capacity_unavailable`; affected jobs say capacity details are unavailable
instead of claiming that no machines are ready. Infrastructure failures while
reading account access or stored diagnostics return an error.

The dashboard shows each queued job's waiting explanation and elapsed wait on
Runs. Machine startup timing remains in Machines → Reliability.
