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.
This page is the contract for AI agents and scripts that operate Manage Runners through manage-runners. It is also available as Markdown, and the whole documentation as one file at /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:
- Install the CLI.
- Give the agent a token, either:
- interactive login with
manage-runners setuporauth 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.
- interactive login with
- Optionally install 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
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
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 |
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 shows the catalog and the help text of every command.
Output
Success:
{
"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:
{
"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 IDsends a request ID (1 to 128 safe characters) to the API and echoes it inmeta.request_id, which helps to correlate logs.--output jsonlwrites 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 resumeandrunner duplicatecreate Hetzner servers, which Hetzner bills to the user.recreate:runner updatemay delete and recreate an active runner’s server, interrupting its jobs.delete:runner pauseandrunner deletedelete servers.runner deletealso 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 NAMEfor a variable with another name. Nothing is stored. auth status --agentreports where the token comes from incredential.source(environment,keyringorfile). 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.
The agent skill
The CLI embeds a skill file that tells agents how to use it safely. Install it for your agent:
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):
---
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.mdto the page URL, for example this page as Markdown. - /llms.txt lists all pages with their Markdown URLs.
- /llms-full.txt contains the whole documentation, including the command reference, in one file.
