# The CLI for AI agents and automation

> Agent mode, command discovery, the JSON contract, exit and error codes, confirmations, scopes, and the embedded agent skill.

Source: https://managerunners.com/docs/cli/agents/

This page is the contract for AI agents and scripts that operate Manage Runners through `manage-runners`. It is also available as [Markdown](https://managerunners.com/docs/cli/agents/index.md), and the whole documentation as one file at [/llms-full.txt](https://managerunners.com/llms-full.txt).

## Roles: the human sets up, the agent operates

A human installs the CLI, logs in and decides what the agent may do:

1. [Install](https://managerunners.com/docs/cli/install/) the CLI.
2. Give the agent a token, either:
   - **interactive login** with `manage-runners setup` or `auth login`, which yields a read-only token for 30 days, plus write scopes only if the human adds them with `--scope`, or
   - **a dashboard token** with exactly the scopes and expiry the task needs, provided to the agent through its secret store. See [Personal access tokens](https://managerunners.com/docs/manual/access-tokens/).
3. Optionally install the [agent skill](#the-agent-skill).

The agent then runs commands with `--agent`, reads the results as JSON, and asks the human before anything that costs money or deletes data.

## Agent mode

```sh
manage-runners runner list --agent
```

`--agent` means JSON output and no prompts. It is the same as `--output json --non-interactive`. In agent mode, a command that would need an answer fails instead of waiting for one.

## Discover commands

```sh
manage-runners commands --agent
manage-runners help runner delete --agent
```

`commands` returns the catalog of every command. Each entry has:

| Field | Meaning |
| --- | --- |
| `name` | Dotted name, for example `runner.delete` |
| `path` | The words to type after `manage-runners`, for example `["runner", "delete"]` |
| `summary` | One-line description |
| `mutation` | `true` if the command changes local configuration or remote state |
| `destructive` | `true` if it deletes or stops something |
| `paid_resource_effect` | `create`, `recreate`, `delete` or `null`: what it does to paid Hetzner resources |
| `required_scopes` | Token scopes the command needs |
| `required_flags` | Flags that must be given |
| `confirmation` | `paid_resource`, `runner_id` or `null`, see [Confirmations](#confirmations) |
| `supports_json` | Whether JSON output is supported |
| `supports_non_interactive` | `false` for commands that need a human at a terminal, such as `setup` |

`help <command> --agent` returns the same entry plus every flag with its usage text and whether it is required. The [command reference](https://managerunners.com/docs/cli/reference/) shows the catalog and the help text of every command.

## Output

Success:

```json
{
  "ok": true,
  "command": "runner.get",
  "data": {
    "profile": "default",
    "origin": "https://api.managerunners.com",
    "runner": { "runnerId": "812999368881481087", "name": "build-01", "state": "READY" }
  },
  "meta": { "schema_version": "1", "cli_version": "1.0.0", "api_version": "1", "request_id": "deploy-42" },
  "warnings": []
}
```

Failure:

```json
{
  "ok": false,
  "command": "runner.delete",
  "error": {
    "type": "usage",
    "code": "USAGE",
    "message": "--yes is required in non-interactive mode",
    "retryable": false,
    "unknown_outcome": false,
    "http_status": null,
    "field_errors": [],
    "details": {}
  },
  "meta": { "schema_version": "1", "cli_version": "1.0.0" },
  "warnings": []
}
```

The examples are abbreviated: the shape of `data` depends on the command, so read it from a real response. Both results and errors are written to standard output; prompts and nothing else go to standard error. Always check the exit code and `ok`.

- `--request-id ID` sends a request ID (1 to 128 safe characters) to the API and echoes it in `meta.request_id`, which helps to correlate logs.
- `--output jsonl` writes one JSON line per item of a list, and one line `{"type": "warning", ...}` per warning. An empty list writes nothing.
- IDs of runners and organizations are decimal strings. Keep them as strings; they do not fit into a double-precision number.

## Exit codes

| Code | Meaning | What to do |
| --- | --- | --- |
| 0 | Success | |
| 2 | Invalid command, flag or value, or a missing confirmation | Fix the command. Nothing was sent. |
| 3 | Token missing, invalid or expired | Ask the human to log in again. |
| 4 | Missing scope, role or plan | Do not retry. See the error code. |
| 5 | Runner, schedule or organization not found, or not monitored | Check the ID and organization. |
| 6 | The runner's state does not allow the action, or a name is ambiguous | Read the runner's state first. |
| 7 | The API rejected the input, or waiting timed out after the change was accepted | Fix the input, or check the runner's state. |
| 8 | Rate limited or temporarily unavailable | Retry later with backoff. |
| 9 | The API could not be reached | Check connectivity. Before repeating a change, check whether it happened. |
| 10 | A change may or may not have happened | Do not retry. Read the state and reconcile first. |
| 11 | `auth logout --revoke` could not confirm revocation or local removal | Check the token in the dashboard. |
| 12 | Configuration or credential store problem | Run `manage-runners doctor`. |
| 13 | Unexpected API response, too large a response, or an unsupported version; for `self-update`, a release or download that failed its checks | Update the CLI; for metrics, use a shorter range. Do not retry a rejected download. |
| 14 | `version --check`: an update is available (`ok` is `true`) | Tell the human; update only with their consent. |
| 70 | Internal error | Report it. |

### Error codes

The most important values of `error.code`:

| Code | Exit | Meaning |
| --- | --- | --- |
| `USAGE` | 2 | Invalid input or missing confirmation |
| `ORGANIZATION_AMBIGUOUS` | 2 | `--org` matches several organizations by name |
| `UNAUTHENTICATED` | 3 | No valid token |
| `FORBIDDEN` | 4 | The token lacks a scope, or the plan does not include the feature |
| `SUBSCRIPTION_REQUIRED` | 4 | Monitoring needs an active plan. The same HTTP 403 is used for a missing scope, so this is the likely, not certain, cause. |
| `ORGANIZATION_PERMISSION_DENIED` | 4 | Your role in the organization does not allow the action |
| `NOT_RELEASE_BUILD` | 2 | A development or modified build cannot run `self-update`; `error.details.hint` names the install script (Linux, macOS) or the manual download steps (Windows) |
| `NOT_FOUND` | 5 | The runner or schedule does not exist in the selected organization |
| `ORGANIZATION_NOT_FOUND` | 5 | The organization is not among your memberships |
| `NOT_MONITORED` | 5 | The runner exists but is not active or has monitoring off |
| `RELEASE_NOT_FOUND` | 5 | The release given to `self-update --version` does not exist |
| `CONFLICT` | 6 | The runner's state does not allow the action |
| `AMBIGUOUS` | 6 | Several runners have that name; use `--id` |
| `VALIDATION` | 7 | The API rejected the input |
| `TIMEOUT` | 7 | Waiting for a runner timed out; the change itself was accepted |
| `RETRYABLE` | 8 | Try again later |
| `NETWORK` | 9 | Network error |
| `UNKNOWN_OUTCOME` | 10 | The change may have happened; reconcile before retrying |
| `REVOCATION_UNCONFIRMED` | 11 | Token revocation could not be confirmed |
| `CONFIG`, `CREDENTIAL_STORE` | 12 | Local configuration or credential store problem |
| `INSTALL_DIR_NOT_WRITABLE`, `UPDATE_FAILED` | 12 | `self-update` cannot write or replace the binary, or the binary changed while it ran; when the directory is not writable, `error.details.hint` names the install script (Linux, macOS) or the manual download steps (Windows) |
| `INCOMPATIBLE_RESPONSE`, `RESPONSE_TOO_LARGE` | 13 | The API response or release manifest could not be used |
| `PLATFORM_NOT_RELEASED` | 13 | The release has no build for this operating system and CPU |
| `DOWNLOAD_REJECTED` | 13 | The download failed its size, SHA-256 or archive checks, or was redirected to another host; nothing was changed |

Warnings in `warnings` do not change the exit code. Examples: `TOKEN_ENV` and `TOKEN_ENV_OVERRIDES_STORE` (a token came from the environment), `RANGE_EXCEEDS_HISTORY` (metrics history is shorter than the range), `RUNNER_WARNING` (a message from the API about the runner).

## Confirmations

In agent mode the CLI never prompts. Commands that would ask a human must be confirmed with flags, and the agent may only add these flags when the human has approved the specific action.

| Catalog `confirmation` | Commands | Non-interactive confirmation |
| --- | --- | --- |
| `paid_resource` | `runner create`, `runner resume`, `runner duplicate`, `runner pause`, and `runner update` when it recreates the server | `--yes` |
| `runner_id` | `runner delete`, `runner forget` | `--yes` and `--confirm <runner-id>`, exactly equal to `--id` |
| `runner_id` | `schedule delete` | `--yes` and `--confirm <runner-id>`, exactly equal to `--runner-id` |
| `self_update` | `self-update` | `--yes` |
| `null` | every other command | none |

`runner update` needs `--yes` only with `--product-id`, `--gitlab-host`, `--runner-token-env`, `--concurrency`, `--executor`, `--ssh-key-id` or `--monitoring`, because those recreate an active runner's server. `--name`, `--label` and `--hetzner-token-env` alone do not.

A missing confirmation fails with exit code 2 before anything is sent. `self-update` reads the release manifest first, so it can report that nothing needs to change, but downloads nothing without confirmation.

Never run `self-update` unless the human asked for it or agreed to it; `version --check` is safe to run.

## Paid and destructive commands

Treat every command with `mutation: true` as potentially billable, and check `paid_resource_effect`:

- `create`: `runner create`, `runner resume` and `runner duplicate` create Hetzner servers, which Hetzner bills to the user.
- `recreate`: `runner update` may delete and recreate an active runner's server, interrupting its jobs.
- `delete`: `runner pause` and `runner delete` delete servers. `runner delete` also deletes the IP addresses and the schedule.

`runner forget` only removes an `UNKNOWN` runner's record and leaves the server untouched. It is never a substitute for `runner delete`.

## Scopes

| Scope | Commands |
| --- | --- |
| `profile:read` | `auth status`, `profile show`, `org list`, `org use`, resolving `--org` |
| `runners:read` | `runner list`, `get`, `watch`, `wait`, `metrics`, `ssh-keys`, `update`, and `--wait` on any runner change |
| `runners:write` | `runner create`, `update`, `pause`, `resume`, `duplicate` |
| `runners:delete` | `runner delete` |
| `runners:forget` | `runner forget` |
| `schedules:read` | `schedule get` |
| `schedules:write` | `schedule set`, `enable`, `disable`, `delete` |
| `products:read` | `product list`, `get`, `locations` |
| `providers:ssh-keys:read` | `provider hetzner ssh-keys` |
| `subscriptions:read` | `subscription show`, `subscription plans` |

`runner update` needs both `runners:read` and `runners:write`, because it reads the runner before writing the merged settings. `--wait` on `create`, `update`, `pause`, `resume` or `duplicate` polls the runner and needs `runners:read` as well. If polling fails, for example with exit code 4 for a missing scope or 7 for a timeout, the change itself was already accepted and is not undone: read the runner before doing anything else.

The token's scopes and the member's role both apply. A token cannot manage billing, organizations, members, invitations or other tokens. Imported tokens keep the scopes they were created with; the CLI cannot add scopes to them.

## Credentials for agents

- Prefer a token passed through the environment: `MANAGE_RUNNERS_TOKEN`, or `--token-env NAME` for a variable with another name. Nothing is stored.
- `auth status --agent` reports where the token comes from in `credential.source` (`environment`, `keyring` or `file`). It never prints the token.
- Never print a token, pass it as a literal argument, or write it into files or logs.
- Use the file credential store (`--credential-store file`) only when the human agreed to it.
- Do not change profiles you were not asked to use.

## Idempotency and unknown outcomes

`runner create` and `runner duplicate` send an idempotency key. The CLI generates a random key for each invocation unless you pass `--idempotency-key` (16 to 128 letters, digits, `.`, `_` or `-`). The API deduplicates a request only when the user, organization, route, key and request are all the same. The 24 hours below count from the first attempt with that key, not from when it finished:

- **Completed first attempt, retried within 24 hours of the first attempt:** the API returns the original result and creates nothing new.
- **Completed first attempt, retried later than that:** the key is forgotten, and the same request creates another runner.
- **First attempt not completed** (still running, or interrupted before its result was stored): the API rejects every retry with that key as a conflict (exit code 6), also after 24 hours. Read the state instead of retrying.
- **Same key, different request:** the API rejects it as a conflict (exit code 6).

So reusing a key only protects a retry made soon after the first attempt, by the same user in the same organization, with the same flags. Never rely on an old key: before retrying a create or duplicate, list the runners and check whether the first attempt already created one.

When a change ends with exit code 10 (`UNKNOWN_OUTCOME`), the change may have happened. `error.details` may contain `retryGuidance` and `idempotencyKey`. Read the runner list or the runner, decide what actually happened, and only then continue.

## Organizations

Without `--org` or a stored organization, commands use the user's default organization. Use `org list --agent` to see memberships and roles, and `--org ID_OR_NAME` for one invocation. Store an organization with `org use` only when asked. See [Profiles and organizations](https://managerunners.com/docs/cli/profiles-and-organizations/).

## The agent skill

The CLI embeds a skill file that tells agents how to use it safely. Install it for your agent:

```sh
manage-runners setup skill install --target claude     # ~/.claude/skills/manage-runners
manage-runners setup skill install --target codex      # ~/.codex/skills/manage-runners
manage-runners setup skill install --target opencode   # opencode skills directory in your user config directory
manage-runners setup skill install --path ./skills/manage-runners
```

`setup skill status` reports whether it is installed and unchanged, and `setup skill remove` removes an unmodified copy. `--dry-run` shows what would change. An existing file that the CLI did not install, or a modified copy, is only replaced with `--force`.

This is the skill embedded in `manage-runners` (skill version 1):

````markdown
---
name: manage-runners
description: Operate manage-runners safely through the official CLI.
metadata:
  owner: manage-runners
  version: "1"
---

# Manage Runners

Use `manage-runners commands --agent` to discover supported operations and required scopes.

Interactive `auth login` and `setup` request `profile:read`, `runners:read`, `schedules:read`, `products:read`, `providers:ssh-keys:read`, and `subscriptions:read` by default. Add write, delete, or forget scopes only when explicitly authorized. PAT imports keep the scopes granted by the server.

When a keyring is unavailable, a set `MANAGE_RUNNERS_TOKEN` is used automatically as the PAT for read and runner commands; `--token-env ENV_NAME` overrides it for one invocation. `auth status --agent` reports the credential source. Never print either variable's value; `auth login --token-env` instead imports a PAT into the credential store.

To store a credential without a keyring, pass `--credential-store file` or set `MANAGE_RUNNERS_CREDENTIAL_STORE=file`. The CLI keeps only a scoped, expiring PAT in a 0600 file, never the password or 2FA secret; tell the user so, and that it can be revoked in the dashboard or with `auth logout --revoke`. Never select the file store without the user's agreement.

Always use `--agent` for automation. Treat mutation commands as potentially billable and require the caller to supply every confirmation requested by command discovery.

`runner update` with `--product-id`, `--gitlab-host`, `--runner-token-env`, `--concurrency`, `--executor`, `--ssh-key-id` or `--monitoring` deletes and recreates an active runner's VM and needs `--yes`; `--name`, `--label` and `--hetzner-token-env` alone do not.

Use `runner metrics --agent` (optionally `--id`/`--name` and `--range 1h|24h|7d|30d|90d`) to read resource monitoring. `SUBSCRIPTION_REQUIRED` means monitoring needs an active paid subscription; do not retry. Only change monitoring when explicitly asked.

Runners, schedules, metrics and the subscription belong to an organization. Without `--org` and a stored organization, commands use the caller's default organization. Use `org list --agent` to see memberships and roles, `--org ID_OR_NAME` for one invocation, or `org use ID_OR_NAME` (`org use --clear`) to store one in the profile only when asked. `ORGANIZATION_PERMISSION_DENIED` means the member's role does not allow the action; do not retry. `ORGANIZATION_NOT_FOUND` means the organization is not among the caller's memberships.

`version --check --agent` reports whether a newer CLI release exists and exits 14 when one does; it is safe to run. Never run `self-update` without the user's consent, and pass `--yes` only after they agreed to that update.

Never print credentials, pass a token as a literal command argument, or change an unrelated manage-runners profile.
````

## Machine-readable documentation

- Every page of these docs is available as Markdown: add `index.md` to the page URL, for example [this page as Markdown](https://managerunners.com/docs/cli/agents/index.md).
- [/llms.txt](https://managerunners.com/llms.txt) lists all pages with their Markdown URLs.
- [/llms-full.txt](https://managerunners.com/llms-full.txt) contains the whole documentation, including the command reference, in one file.
