---
title: "Commands"
description: "API reference for running commands on Alpacon servers. Run, list, read and cancel commands, with scheduling, dependencies, file execution and error codes."
---

> Full documentation index: https://docs.alpacax.com/llms.txt

# Commands

**Endpoint**: `/api/events/commands/`

Run commands on servers and read their results. A command runs in one of two ways:

- **Through a service token**, under the token's server and command access-control rules. This is the way for automation. See [Service tokens](/use/iam/applications/service-tokens/).
- **Inside a work session**, for a person signed in to the console or the CLI. Name the session in `work_session`; without one the request is refused with `work_session_required`. See [Work sessions](/use/work-sessions/).

Personal API tokens cannot be issued command execution scopes or file-transfer scopes. Use a [service token](/use/iam/applications/service-tokens/) for automation and CI.

The examples on this page use a service token.

  
    ## List commands

    Retrieve a paginated list of commands.

    ### Request

    ```http
    GET /api/events/commands/
    ```

    ### Query parameters

    | Parameter | Type | Description |
    |-----------|------|-------------|
    | `server` | UUID | Filter by server ID |
    | `work_session` | UUID | Filter by work session ID |
    | `id__in` | string | Filter by command IDs (comma-separated UUIDs, up to 100) |
    | `shell` | string | Filter by shell |
    | `requested_by` | integer | Filter by requesting user ID |
    | `requested_by_isnull` | boolean | `true` for commands with no requesting user |
    | `service_token` | UUID | Filter by the service token that sent the command |
    | `application` | UUID | Filter by the application whose service token sent the command |
    | `api_token` | UUID | Filter by the personal API token that sent the command |
    | `request_channel` | string | How the command was sent: `web`, `cli`, `mcp`, `api_token`, `service_token`, `slack`, `unknown_client` or `unknown`. Comma-separate several to match any |
    | `declared_agent` | string | Commands whose client declared this agent name |
    | `agent_command` | boolean | `true` for commands sent by an AI agent or a token, `false` for the rest |
    | `is_scheduled` | boolean | `true` for commands scheduled in the future, `false` for the rest |
    | `verification_status` | string | Filter by AI [verification status](/use/audit/events/commands/#verification-status): `pending`, `awaiting_purpose`, `awaiting_approval`, `approved`, `rejected`, `skipped`, `monitor_executed` or `advisory` |
    | `risk_level` | string | Filter by risk level: `INFO`, `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. Case-insensitive; comma-separate several (`HIGH,CRITICAL`). An unknown value returns `400` with `invalid_input` |
    | `refused` | boolean | `true` for commands that were refused: rejected, or denied by a command access-control rule |
    | `denial_cause` | string | Refused commands by group of `rejection_cause`: `not_allowed` (`command_acl_denied`, `command_acl_unavailable`), `risk`, `approval` or `integrity_system` |
    | `override_reason` | string | Use `session_less_service_token` to find commands that ran after a rejecting verdict under the session-less service-token policy. Other values return `400` with `invalid_choice` |
    | `credential_deleted` | boolean | `true` for token commands whose token has since been deleted |
    | `credential_exposure_acknowledged` | boolean \| null | Commands sent with `credential_exposure_acknowledged` |
    | `added_at__gte` | datetime | Commands created at or after this time (ISO 8601) |
    | `added_at__lt` | datetime | Commands created before this time (ISO 8601). With `added_at__gte`, a half-open range, so back-to-back ranges never count a command twice |
    | `search` | string | Search by server name, command line, or requester name |
    | `ordering` | string | Sort field (default: `-scheduled_at`). Options: `added_at`, `scheduled_at`, `delivered_at`, `acked_at`, `handled_at` |
    | `page` | integer | Page number |
    | `page_size` | integer | Results per page (default: 15, max: 100) |

    There is no filter on `status`; filter by `verification_status`, or read `status` from the results.

    ### Response

    ```json
    {
      "count": 85,
      "current": 1,
      "next": 2,
      "previous": null,
      "last": 6,
      "results": [
        {
          "id": "550e8400-e29b-41d4-a716-446655440000",
          "shell": "system",
          "line": "df -h",
          "success": true,
          "exit_code": 0,
          "result": "Filesystem  Size  Used Avail Use% Mounted on\n/dev/xvda1  20G  8.5G  11G  46% /",
          "status": "completed",
          "cancellable": false,
          "response_delay": 0.125,
          "elapsed_time": 0.023,
          "added_at": "2026-10-20T10:30:44Z",
          "scheduled_at": "2026-10-20T10:30:44Z",
          "server": {
            "id": "7e3984de-49ab-4cc6-bcdf-21fbd35858b8",
            "name": "web-server-01",
            "os": "Ubuntu 22.04.4 LTS",
            "platform": "debian",
            "is_connected": true,
            "is_deleted": false
          },
          "username": "deploy",
          "groupname": "alpacon",
          "requested_by": null,
          "request_channel": "service_token",
          "work_session": null,
          "service_token": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
          "service_token_name": "ci-deploy",
          "application": "9d2e4b71-0c3a-4f5e-8b6d-1a2c3e4f5a6b",
          "application_name": "CI pipeline",
          "risk_level": "LOW",
          "verification_status": "approved",
          "override_reason": null,
          "purpose": "Check disk space before deploying"
        }
      ]
    }
    ```

    The list returns more fields than shown here, such as `declared_agent` and `rejection_cause`. `override_reason` is a read-only `string | null` in list and detail responses and in work-session timeline command items. Its only current non-null value is `session_less_service_token`: a command sent with a service token outside a work session ran even though AI verification rejected it. Such a command has `verification_status: "monitor_executed"`. Other commands have `override_reason: null`, including commands that ran under signing monitor mode with the same verification status.

    ### Example

    ```bash
    curl "https://your-workspace.us1.alpacon.io/api/events/commands/?ordering=-scheduled_at&page_size=20" \
      -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\""
    ```

    ### Bulk status lookup

    When tracking many commands at once (for example, a dashboard or automation polling for results), pass a comma-separated list of command IDs to `id__in` instead of issuing one `GET /api/events/commands/{command_id}/` per command. Set `page_size=100` as well: the default page size is 15, so without it a request for more than 15 IDs returns only the first page, with `next` set to `2`.

    ```bash
    curl "https://your-workspace.us1.alpacon.io/api/events/commands/?id__in=550e8400-e29b-41d4-a716-446655440000,9b52a7c4-13de-48f6-a01b-7f3e5d982c44&page_size=100" \
      -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\""
    ```

    Up to 100 IDs can be requested at once; exceeding this limit returns `400` with `event_command_bulk_limit_exceeded`, and any value that is not a valid UUID returns `400`. An empty `id__in` value is ignored: the request behaves as if the parameter were omitted and returns the full accessible list. Results follow the `ordering` parameter (default: `-scheduled_at`), not the order of the IDs in the request, so match each result to your list by its `id` field. IDs for commands you cannot access or that no longer exist are silently excluded; compare the response `count` with the number of unique IDs you requested to detect exclusions.
  </Tabs.Tab>

  <Tabs.Tab>
    ## Run command

    Run a command on one or more servers. Send `server` for one server or `servers` for several; Alpacon creates one command per server.

    ### Request

    ```http
    POST /api/events/commands/
    ```

    ### Request parameters

    | Field | Type | Required | Description |
    |-------|------|----------|-------------|
    | `server` | UUID | Yes* | Target server ID |
    | `servers` | array[UUID] | Yes* | Target server IDs; one command is created per server |
    | `line` | string | Yes** | Command to run, up to 2,048 characters |
    | `file` | object | Yes** | A script to run instead of `line`. See [Run a script](#run-a-script) |
    | `purpose` | string | No | Why you are running it, up to 2,000 characters. Shown to the approver if the command is held, and used when the AI judges it |
    | `work_session` | UUID | No | The work session to run in. Required for a person signed in to the console or the CLI; not needed with a service token |
    | `username` | string | No | Server account to run as. Running as an account that isn't yours asks for MFA (see the errors below) |
    | `groupname` | string | No | Server group to run as (default: `alpacon`) |
    | `env` | object | No | Environment variables. Write-only: never returned and never recorded in the command line |
    | `data` | string | No | Additional data for the command |
    | `scheduled_at` | datetime | No | When to run it (default: now) |
    | `run_after` | array[UUID] | No | Commands that must finish successfully first, up to 100 |
    | `credential_exposure_acknowledged` | boolean \| null | No | Let a command whose line contains an inline credential run anyway; recorded in the audit log |

    \* Send `server` or `servers`.
    \*\* Send `line`, or `file` for a script.

    <Callout type="info">
    `line` is refused with `400 command_inline_credential` if it contains an inline credential (a secret-like `KEY=VALUE` pair, a password flag, or a connection string with an embedded password) before the command is stored. Pass secrets through `env` instead. To run this one command anyway, set `credential_exposure_acknowledged: true`, which is recorded as an audit entry on the command.
    </Callout>

    ### Request body

    ```json
    {
      "server": "7e3984de-49ab-4cc6-bcdf-21fbd35858b8",
      "line": "df -h",
      "username": "deploy",
      "purpose": "Check disk space before deploying",
      "env": {
        "CUSTOM_VAR": "value"
      }
    }
    ```

    ### Request body (several servers)

    ```json
    {
      "servers": [
        "7e3984de-49ab-4cc6-bcdf-21fbd35858b8",
        "8f4985df-50bc-42e5-b827-32fcc6655111"
      ],
      "line": "systemctl status nginx",
      "username": "deploy"
    }
    ```

    ### Response

    `201 Created`, with a list: one command per target server, even for a single `server`.

    ```json
    [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "line": "df -h",
        "purpose": "Check disk space before deploying",
        "data": null,
        "username": "deploy",
        "groupname": "alpacon",
        "status": "verifying",
        "cancellable": true,
        "added_at": "2026-10-20T10:30:44Z",
        "scheduled_at": "2026-10-20T10:30:44Z",
        "server": {
          "id": "7e3984de-49ab-4cc6-bcdf-21fbd35858b8",
          "name": "web-server-01",
          "os": "Ubuntu 22.04.4 LTS",
          "platform": "debian",
          "is_connected": true,
          "is_deleted": false
        },
        "requested_by": null,
        "request_channel": "service_token",
        "run_after": [],
        "work_session": null,
        "service_token": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
        "service_token_name": "ci-deploy",
        "application": "9d2e4b71-0c3a-4f5e-8b6d-1a2c3e4f5a6b",
        "application_name": "CI pipeline"
      }
    ]
    ```

    A request with a single `server` requires a connected, commissioned server; otherwise it returns `400 server_not_connected` or `400 server_not_commissioned`. A request with `servers` stores a command for an offline server to run when it reconnects. Poll [Get command](#get-command), or the list with `id__in`, for the result.

    ### Example

    ```bash
    curl -X POST "https://your-workspace.us1.alpacon.io/api/events/commands/" \
      -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\"" \
      -H "Content-Type: application/json" \
      -d '{"server": "7e3984de-49ab-4cc6-bcdf-21fbd35858b8", "line": "df -h", "username": "deploy"}'
    ```

    The token needs the `command:create` scope, a server access-control rule that covers the server, and a command rule that allows the line. See [Command patterns and sudo](/use/iam/applications/service-tokens/command-patterns/).

    ### Run a script

    Send `file` instead of `line` to run a script. Alpacon AI reviews the script before it runs.

    | Field | Type | Required | Description |
    |-------|------|----------|-------------|
    | `file.path` | string | Yes | Absolute path the script is written to on the server |
    | `file.interpreter` | string | Yes | Absolute path of the interpreter, such as `/bin/bash` |
    | `file.content` | string | Yes | The script, up to 64 KB |
    | `file.args` | array[string] | No | Arguments, up to 64 |
    | `file.reuse_days` | integer | No | Ask the approver to let this unchanged script run again without a new approval, for this many days (1 to 366, within the workspace's limit) |

    A file request can't carry `line`, `data` or `env`.
  </Tabs.Tab>

  <Tabs.Tab>
    ## Get command

    Retrieve full details and results for a specific command.

    ### Request

    ```http
    GET /api/events/commands/{command_id}/
    ```

    ### Path parameters

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `command_id` | UUID | Yes | Command ID |

    ### Response

    The list fields, plus the delivery timestamps, the AI's judgment and, for a held or refused command, why.

    ```json
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "shell": "system",
      "line": "df -h",
      "data": null,
      "success": true,
      "exit_code": 0,
      "result": "Filesystem  Size  Used Avail Use% Mounted on\n/dev/xvda1  20G  8.5G  11G  46% /",
      "status": "completed",
      "cancellable": false,
      "response_delay": 0.125,
      "elapsed_time": 0.023,
      "added_at": "2026-10-20T10:30:44Z",
      "scheduled_at": "2026-10-20T10:30:44Z",
      "delivered_at": "2026-10-20T10:30:44.500Z",
      "acked_at": "2026-10-20T10:30:44.625Z",
      "handled_at": "2026-10-20T10:30:44.648Z",
      "server": {
        "id": "7e3984de-49ab-4cc6-bcdf-21fbd35858b8",
        "name": "web-server-01",
        "os": "Ubuntu 22.04.4 LTS",
        "platform": "debian",
        "is_connected": true,
        "is_deleted": false
      },
      "username": "deploy",
      "groupname": "alpacon",
      "requested_by": null,
      "request_channel": "service_token",
      "work_session": null,
      "service_token": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
      "service_token_name": "ci-deploy",
      "verification_status": "approved",
      "risk_level": "LOW",
      "purpose": "Check disk space before deploying",
      "rejection_cause": null,
      "rejection_gate": null,
      "rejected_at": null,
      "error_phase": null,
      "denial_code": null,
      "sudo_approval_request_id": null,
      "sudo_grant_status": null,
      "approved_file_execution": null,
      "file_execution_suggestion": null
    }
    ```

    ### Response fields

    | Field | Type | Description |
    |-------|------|-------------|
    | `id` | UUID | Command ID |
    | `shell` | string | `system` for a command line, `file` for a script sent with `file`, `internal` for an action Alpacon sends to the agent itself |
    | `line` | string | The command line, with secrets masked |
    | `data` | string \| null | Additional data sent with the command. `null` for file-transfer commands and commands that open a terminal, tunnel or file session, and for firewall commands unless you are a Superuser |
    | `success` | boolean \| null | Whether the command succeeded; `null` until it finishes |
    | `exit_code` | integer \| null | The command's exit code; `null` until it finishes, or when it ended without one |
    | `result` | string \| null | The command's output, with secrets masked |
    | `status` | string | See [Command status](#command-status) |
    | `cancellable` | boolean | Whether the command can still be cancelled |
    | `response_delay` | number \| null | Seconds from delivery until the server acknowledged the command |
    | `elapsed_time` | number \| null | Seconds the command ran on the server |
    | `added_at`, `scheduled_at`, `delivered_at`, `acked_at`, `handled_at` | datetime \| null | Progress timestamps; see [Command status](#command-status) |
    | `server` | object | The target server |
    | `username`, `groupname` | string | The server account and group the command runs as |
    | `requested_by` | object \| null | The person who sent the command, also for a personal API token; `null` for a service-token command |
    | `request_channel` | string | How the command was sent, with the same values as the `request_channel` filter |
    | `declared_agent` | string | The agent name the sending client declared, if any. Self-reported, not verified |
    | `work_session` | object \| null | The work session the command ran in. `null` outside a session or for a session you can't view |
    | `api_token`, `api_token_name` | string \| null | The personal API token that sent the command, and its name |
    | `service_token`, `service_token_name` | string \| null | The service token that sent the command, and its name |
    | `application`, `application_name` | string \| null | The application whose service token sent the command, and its name |
    | `verification_status` | string | AI [verification status](/use/audit/events/commands/#verification-status), with the values of the `verification_status` filter |
    | `risk_level` | string | `INFO`, `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`; empty when the AI didn't judge it |
    | `analyzed_at` | datetime \| null | When the AI judged it |
    | `rejected_at` | datetime \| null | When it was rejected |
    | `rejection_cause`, `rejection_gate` | string \| null | Why a `rejected` command was refused; see [Rejection causes](#rejection-causes) |
    | `denial_code` | string \| null | Why a `denied` command was stopped; see [Denial codes](#denial-codes) |
    | `error_phase` | string \| null | Where a failed or stuck command went wrong; see [Error phases](#error-phases) |
    | `override_reason` | string \| null | `session_less_service_token` when the command ran although AI verification rejected it; see [List commands](#list-commands) |
    | `purpose` | string | The reason the requester gave |
    | `purpose_requested_at`, `purpose_expires_at` | datetime \| null | When Alpacon asked the requester for a purpose, and until when they can answer. `purpose_expires_at` is `null` unless the question is still open |
    | `signature` | string \| null | Alpacon AI's signature on an approved command |
    | `key_id` | string | The key that made `signature`; empty when there is none |
    | `credential_exposure_acknowledged` | boolean \| null | Whether the command was sent with `credential_exposure_acknowledged` |
    | `sudo_approval_request_id`, `sudo_grant_status` | string \| null | Where the command's `sudo` approval stands; see [Sudo approval](#sudo-approval) |
    | `approved_file_execution` | object \| null | The standing approval that let a script run without a new approval; see [Script fields](#script-fields) |
    | `file_execution_suggestion` | object \| null | How to send a held script command again as a script; see [Script fields](#script-fields) |

    The list returns a subset of these fields, plus `denied`. It leaves out `data`, `delivered_at`, `acked_at`, `handled_at`, `analyzed_at`, `rejected_at`, `purpose_requested_at`, `purpose_expires_at`, `signature`, `key_id`, `sudo_approval_request_id`, `sudo_grant_status` and `file_execution_suggestion`; read those from this route.

    ### Example

    ```bash
    curl "https://your-workspace.us1.alpacon.io/api/events/commands/550e8400-e29b-41d4-a716-446655440000/" \
      -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\""
    ```
  </Tabs.Tab>

  <Tabs.Tab>
    ## Cancel command

    Cancel a command that hasn't been delivered to the server yet. `cancellable` on the command says whether this is still possible.

    ### Request

    ```http
    DELETE /api/events/commands/{command_id}/
    ```

    ### Path parameters

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `command_id` | UUID | Yes | Command ID |

    ### Response

    ```http
    204 No Content
    ```

    ### Example

    ```bash
    curl -X DELETE "https://your-workspace.us1.alpacon.io/api/events/commands/550e8400-e29b-41d4-a716-446655440000/" \
      -H "Authorization: token=\"alpst-xxxxxxxxxxxxxxxxxx\""
    ```
  </Tabs.Tab>
</Tabs>

## Command status

`status` is a string that Alpacon works out each time you read the command:

| Status | Meaning |
|--------|---------|
| `scheduled` | Waiting for its `scheduled_at` time |
| `queued` | Due, and waiting to be delivered, for example until the server reconnects or a `run_after` command finishes |
| `verifying` | Alpacon AI is judging it |
| `awaiting_purpose` | Waiting for the requester to say what the command is for |
| `awaiting_approval` | Held for an approver |
| `delivered` | Sent to the server |
| `running` | The server started it |
| `stuck` | Delivered or started, with no progress for a long time |
| `completed` | Finished. Read `success` and `exit_code` for the outcome |
| `rejected` | Refused by a verdict before it ran: the AI, an approver, a workspace rule, or the token's command rules. `rejection_cause` says which, for example `risk_denied`, `approval_rejected`, `approval_policy_denied` or `command_acl_denied` |
| `denied` | Stopped before it ran without a verdict: the token's access-control rules refused it when it was sent, the agent session's approved account doesn't cover it, or its work session ended first. `denial_code` says why; the list response also has `denied: true`. Not the same as `rejected`. A token command its command rules refuse is `rejected` on Alpacon Cloud and `denied` on a self-hosted deployment; see [Refused by the token's command rules](#refused-by-the-tokens-command-rules) |
| `error` | The command's state couldn't be determined |

On the detail route, read `rejection_cause`, `rejection_gate` and `rejected_at` for a `rejected` command, and `denial_code` for a `denied` one. For a `completed` command, read `success` first, then `exit_code`, `result`, `elapsed_time` and `handled_at`; when `success` is `false`, `error_phase` can say where it failed. A stuck command that times out ends as `completed` with `success: false`, `exit_code: null` and a timeout message in `result`.

`cancellable` is a separate boolean: `true` while the command can still be cancelled.

Each command also records its progress in timestamps:

| Field | Description |
|-------|-------------|
| `added_at` | Command created |
| `scheduled_at` | Scheduled run time |
| `delivered_at` | Sent to the server |
| `acked_at` | The server acknowledged it and started running it |
| `handled_at` | Finished |

### Rejection causes

`rejection_cause` uses the values below. `rejection_gate` is `null` for the two signature causes and for `account_bound_to_other_person`.

| `rejection_cause` | `rejection_gate` | Meaning |
| --- | --- | --- |
| `command_acl_denied` | `command_acl` | The command is not pre-approved by the token’s command rules |
| `command_acl_unavailable` | `command_acl` | The command pre-approval decision is unavailable |
| `risk_denied` | `risk` | Risk control refused the command |
| `risk_unavailable` | `risk` | The risk judgment is unavailable |
| `approval_policy_denied` | `approval` | The approval policy refused it or no eligible approver other than the requester is available |
| `approval_rejected` | `approval` | An approver rejected the request |
| `approval_expired` | `approval` | The pending approval expired |
| `approval_cancelled` | `approval` | The requester cancelled the approval request |
| `work_session_terminated` | `work_session` | The work session ended while the command was held |
| `account_bound_to_other_person` | `null` | The run-as account became linked to another person before delivery |
| `unsigned_file` | `null` | The file command has no signature |
| `signature_invalid` | `null` | The agent reported an invalid signature |
| `use_file_execution` | `approval` | An access rule requires the script to run through file execution |

### Denial codes

`denial_code` is `null` unless the command was denied. A client can branch on these fixed values:

| `denial_code` | Meaning |
| --- | --- |
| `command_acl_no_match` | No command rule on the token allows the line |
| `server_acl_denied` | The token's server rules leave out the server |
| `acl_not_configured` | The token has no server or command rules |
| `command_rejected_by_parser` | The line uses shell syntax no command rule can allow |
| `work_session_terminated` | The work session ended before the command was delivered |
| `account_bound_to_other_person` | The run-as account became linked to another person before delivery |
| `work_session_account_not_approved` | The agent's work session doesn't approve the run-as account |
| `account_not_resolved` | The command names no account or group to run as |
| `account_group_not_permitted` | The requester may not run as that group |
| `denied` | Denied for another reason |

For a work session you can't view, or one that no longer exists, `work_session_terminated` reads as `denied`.

### Error phases

`error_phase` says where a command went wrong when `success` is `false` or the command is `stuck`. It is `null` for a command that succeeded, and for a command that ran and exited non-zero for its own reasons, such as `exit 1`.

| `error_phase` | When |
| --- | --- |
| `agent_disconnected` | `stuck`: the server never acknowledged the command, likely because it disconnected |
| `agent_timeout` | `stuck`: the server acknowledged the command but hasn't reported a result |
| `remote_command_exceeded_timeout` | The command ran past its time limit on the server (exit code `124`) |
| `remote_command_not_executable` | The command was found but couldn't be run (exit code `126`) |
| `remote_command_not_found` | The command wasn't found on the server (exit code `127`) |
| `remote_command_killed` | The command was killed by a signal |
| `dependency_cancelled` | The command never reached the server because a `run_after` command it waited on failed or was refused |
| `file_hash_mismatch` | Script: the file changed after it was checked, so the server refused to run it. Send it again so the current file is checked |
| `file_payload_invalid` | Script: the server couldn't read the request. Send it again; if it keeps failing, upgrade the agent |
| `file_open_failed` | Script: the server couldn't open the file. Check the path exists and is a readable regular file |
| `file_exec_unsupported` | Script: this server's platform can't run scripts. Send a command line instead |
| `file_too_large` | Script: the file is larger than the server will run |

`alpacon exec` prints the phase on stderr, with a description for most phases and a next step for the script phases. The MCP command tools add a similar next step as `error_phase_hint`.

### Sudo approval

A command that runs `sudo` can wait for an approver. Two detail fields follow that wait:

- `sudo_grant_status` is `null` when the command has no `sudo` request. Otherwise it is `pending_mfa` (waiting for the requester's MFA check), `pending_approval` (waiting for an approver), `authorized` (approved), `used` (the approved `sudo` ran), `rejected` or `expired`.
- `sudo_approval_request_id` names the [approval request](/reference/approvals/) while `sudo_grant_status` is `pending_approval`, and is `null` otherwise. It turns `null` both when the request is approved and when it is rejected, so read `sudo_grant_status` to tell the two apart.

`alpacon exec` waits on these fields when a `sudo` command needs approval. Both are detail-only; poll [Get command](#get-command) while you wait.

### Script fields

`approved_file_execution` is set when a [standing approval](/use/audit/approved-file-executions/) let a script run without a new approval, and stays set after that approval is revoked or expires. With the `approved_file_execution:read` permission it carries `id`, `expires_at`, `approved_by` (`id`, `username`), `approval_request` (`id`), either of which can be `null`, `revoked_at` and `revoke_reason` (`manual`, `approver_removed`, `approver_deactivated`, `expired`, or empty). Without it, only `id`.

`file_execution_suggestion` is set while a command line that runs a script file is held (`awaiting_approval` or `awaiting_purpose`), and the line could be sent again as a script request. It carries `path`, `args`, `interpreter` (an absolute path, or `null`), `interpreter_name` (a bare name such as `bash`, or `null`; supply the absolute path yourself before sending) and `repeatedly_approved`, which is `true` when people approved this exact line often enough recently to make it a candidate for a standing approval. To send it again, add the script's `content` and copy the server, `username` and `groupname` from the held command. It is `null` when masking changed the line, or when the script request would be refused anyway.

## Error responses

| Status | Error code | Description |
|--------|------------|-------------|
| 400 | `api_insufficient_data` | No target server, or no `line` or `file` |
| 400 | `does_not_exist` | On `server`: the server is outside the service token's server access rules, or does not exist |
| 400 | `server_not_connected` | The single `server` is not connected |
| 400 | `server_not_commissioned` | The single `server` is not commissioned |
| 400 | `command_inline_credential` | `line` contains an inline credential. Move it to `env`, or set `credential_exposure_acknowledged: true` to proceed |
| 400 | `work_session_required` | A person signed in to the console or the CLI sent a command without `work_session` |
| 400 | `work_session_not_active`, `work_session_expired`, `work_session_scope_not_allowed`, `work_session_server_not_allowed` | The work session isn't active, has ended, doesn't include commands, or doesn't include this server |
| 400 | `event_command_bulk_limit_exceeded` | More than 100 command IDs passed to `id__in` |
| 400 | `event_command_run_after_too_many` | More than 100 commands in `run_after` |
| 400 | `file_exec_content_too_large`, `file_exec_empty_content`, `file_exec_invalid_path`, `file_exec_line_not_allowed`, `file_exec_data_not_allowed`, `file_exec_env_not_allowed` | The `file` request is invalid: the script is over 64 KB or empty, a path isn't absolute, or it carries `line`, `data` or `env` |
| 400 | `event_command_already_sent` | Cancel: the command was already delivered to the server |
| 400 | `event_command_not_cancellable` | Cancel: a firewall command can't be cancelled |
| 401 | `auth_mfa_required` | The command runs as an account that isn't yours, and needs a recent MFA check. The body also carries `"source": "command"` |
| 403 | `api_token_scope_missing` | The token lacks `command:create` (or `command:read` to read). `missing` names the scope |
| 403 | `api_token_acl_not_allowed` | On a self-hosted deployment, the token's command rules don't allow the line. Retrying does not help—widen the token's rules |
| 403 | `event_command_not_requester` | Cancel: only the requester or a Superuser can cancel a firewall command |
| 404 | `api_not_found` | Command not found |

Every error body is coded; see [Error messages](/reference/api/#error-messages).

### Refused by the token's command rules

A command the token's command rules don't allow never runs. How it is refused depends on the deployment:

- **Alpacon Cloud**: the request is accepted with `201`, and the command is then refused before it runs: its `status` becomes `rejected` with `rejection_cause` `command_acl_denied`. If the rules couldn't be checked at that moment, `rejection_cause` is `command_acl_unavailable` instead, and sending the command again can succeed.
- **Self-hosted**: the request is refused with `403` and `api_token_acl_not_allowed`, and the command is recorded with `status` `denied`. A self-hosted deployment whose command rules have been moved to Alpacon AI behaves like Alpacon Cloud.

For `command_acl_denied` or `api_token_acl_not_allowed`, retrying does not help—widen the rule. See [Command patterns and sudo](/use/iam/applications/service-tokens/command-patterns/#write-the-rule-exactly-as-you-run-it).

Source: https://docs.alpacax.com/reference/api/commands/
