> For AI agents: the documentation index is at https://docs.gethandup.dev/llms.txt. Append .md to any page URL (without the trailing slash) for Markdown, for example https://docs.gethandup.dev/cli.md.

# run_stop

`POST /v1/requests/{id}/run/stop`

Stop a desktop run from any client: local callers, and paired devices with decide scope. Audited as `run_stop_requested`.

## Parameters

### Path Parameters

- **id** (required): `string`

## Responses

### 200

Stop requested: `run.stop` names who asked and `request.updated` is emitted; the desktop app running the command stops it (TERM, then KILL after 5 s) and reports `stopped from <device>; terminated|killed`. Repeating it changes nothing

Media type: `application/json`

`object`

- **id**: `string`
- **title** (required): `string`
- **tool**: `string | null`
- **callback_url**: `string | null`

  Terminal-event destination; requires the daemon callbacks.allowlist.

- **summary**: `string | null`
- **source**: `object`

  - **integration**: `string | null`, default: `null`

    Server-verified integration token name; caller values are overwritten.

  - **agent**: `string | null`, default: `null`
  - **session**: `string | null`, default: `null`
  - **session_title**: `string | null`, default: `null`

    Display-only session name; scoped rules always use the session ID.

  - **cwd**: `string | null`, default: `null`
  - **repo**: `string | null`, default: `null`
  - **branch**: `string | null`, default: `null`
  - **host**: `string | null`, default: `null`

- **kind**: `string`

  Allowed values: `command` `edit` `review` `question` `info` `custom`

- **risk**: `string`

  Allowed values: `low` `medium` `high`

- **previews**: `Array<object>`

  - **type** (required): `string`

    Allowed values: `text` `markdown` `code` `diff` `files` `command` `json` `html` `image` `video` `audio` `file` `pdf` `email`

  - **title**: `string | null`
  - **lang**: `string | null`
  - **inline**: `string | null`
  - **blob**: `string | null`
  - **mime**: `string | null`
  - **name**: `string | null`
  - **size**: `integer | null` format: int64, >= 0
  - **files**: `Array<object> | null`

    - **path** (required): `string`
    - **blob** (required): `string`
    - **mime** (required): `string`
    - **size** (required): `integer` format: int64, >= 0
    - **lang**: `string | null`

  - **entry**: `string | null`
  - **command**: `object | null`

    - **shell**: `string | null`, default: `null`
    - **argv**: `Array<string> | null`, default: `null`
    - **cwd**: `string | null`, default: `null`
    - **env_keys**: `Array<string>`, default: `[]`

  - **email**: `object | null`

    An email draft for review. handup never sends it: on approval the agent
    sends the draft (or the edited `decision.fields`) with its own mail tool.

    - **from**: `string | null`, default: `null`
    - **to**: `Array<string>`, default: `[]`
    - **cc**: `Array<string>`, default: `[]`
    - **bcc**: `Array<string>`, default: `[]`
    - **reply_to**: `string | null`, default: `null`
    - **subject**: `string`, default: ``
    - **date**: `string | null`, default: `null`

      Date shown in the header, as the agent formats it (e.g. RFC 2822).

    - **body**: `string`, default: ``

      Message body as Markdown (plain text renders as-is).

    - **body_html**: `string | null`, default: `null`

      Optional HTML alternative; rendered only in the sandboxed HTML preview frame.

    - **attachments**: `Array<object>`, default: `[]`

      - **name** (required): `string`
      - **mime**: `string | null`
      - **size**: `integer | null` format: int64, >= 0
      - **blob**: `string | null`

        Uploaded blob hash (`POST /v1/blobs`) so the reviewer can open the file.

    - **in_reply_to**: `object | null`, default: `null`

      The message this draft replies to, shown collapsed under the body.

      - **from**: `string | null`, default: `null`
      - **date**: `string | null`, default: `null`
      - **body**: `string`, default: ``

        Plain text of the earlier message (or a snippet of it).

  - **before_blob**: `string | null`
  - **after_blob**: `string | null`

- **options**: `Array<object>`

  - **id** (required): `string`
  - **label** (required): `string`
  - **outcome** (required): `string`

    Allowed values: `approve` `deny`

  - **style**: `string | null`

- **input**: `object | null`

  Request `input`. Only `kind: question` requests interpret `questions`;
  other keys (for example raw agent tool input) pass through unchanged.

  - **feedback**: `string | null`

    `optional` (default) or `required_on_deny`.

  - **fields**: `Array<any> | null`

    Generic form fields (`text`, `textarea`, `select`, `boolean`) for non-question requests.

  - **questions**: `Array<object> | null`

    Structured questions (1..=10) for `kind: question`; answers arrive in `decision.fields.answers`.

    A question for the human; answered in `decision.fields.answers[id]`.

    - **id** (required): `string`

      Stable id, unique within the request.

    - **header**: `string | null`

      Short topic chip, e.g. "Database".

    - **question** (required): `string`

      The question itself (markdown).

    - **multi**: `boolean`

      Allow selecting several options.

    - **recommended**: `integer | null`, >= 0

      Index of the suggested option.

    - **allow_free_text**: `boolean`

      Allow a typed answer besides (or instead of) the options.

    - **options**: `Array<object>`

      One selectable answer to a [`Question`].

      - **label** (required): `string`

        Answer text; unique within its question and returned in `selected`.

      - **description**: `string | null`

        One-line explanation shown under the label.

      - **preview**: `string | null`

        Markdown shown while the option is focused (code blocks allowed).

- **timeout**: `string | null`

  Optional positive duration, at most one year. Absent uses config; null/`none` disables expiry.

- **run_timeout**: `string | null`

  Optional limit for the desktop app's Run of this command (e.g. `30m`).
  Absent uses the daemon's `run.timeout`; the daemon caps it at
  `run.max_timeout`. Not part of the request's decision timeout.
  Must be between 1ms and one year.

- **on_timeout**: `string`

  Allowed values: `deny` `approve` `expire`

- **dedupe_key**: `string | null`
- **status**: `string`

  Allowed values: `pending` `approved` `denied` `answered` `dismissed` `expired` `cancelled`

- **content_hash**: `string`
- **created_at**: `integer` format: int64, >= 0
- **expires_at**: `integer | null` format: int64, >= 0
- **decision**: `object | null`

  - **option** (required): `string`
  - **feedback**: `string | null`
  - **fields**: `object | null`

    Decision `fields`. Question requests carry only `answers`, keyed by question
    id; other requests carry answers to `input.fields` or edited tool input.

    - **answers**: `object | null`

  - **content_hash** (required): `string`
  - **decided_by**: `string | null`
  - **decided_at**: `integer | null` format: int64, >= 0
  - **outcome**: `string | null`

    Allowed values: `approve` `deny`

  - **rule_id**: `string | null`
  - **scope**: `string | null`
  - **run_result**: `object | null`

    The desktop app ran the request's command for the human: this is how
    it ended. Present only on approved command requests decided by Run.

    - **exit_code**: `integer | null` format: int32

      Process exit status; absent when it never started or a signal ended it.

    - **stdout_tail**: `string`

      Last bytes of standard output (at most 64 KiB), credentials redacted.

    - **stderr_tail**: `string`

      Last bytes of standard error (at most 64 KiB), credentials redacted.

    - **duration_ms**: `integer` format: int64, >= 0

      Wall time from start to exit, in milliseconds.

    - **truncated**: `boolean`

      Earlier output was dropped from either tail.

    - **elevated**: `boolean`

      Ran with administrator rights (pkexec on Linux, osascript on macOS).

    - **error**: `string | null`

      Why it did not finish normally: timeout, cancellation, signal, or a
      start failure (for example pkexec missing or authentication refused).

- **run**: `object | null`

  Present while the desktop app runs the command (daemon-managed).

  - **started_at** (required): `integer` format: int64, >= 0

    Unix milliseconds when the run was claimed.

  - **lease_until** (required): `integer` format: int64, >= 0

    Unix milliseconds after which an unreported run no longer holds the
    request (the app crashed or lost the daemon): the run limit plus
    [`RunClaim::GRACE_MS`].

  - **elevated**: `boolean`

    Running with administrator rights.

  - **timeout_ms**: `integer` format: int64, >= 0

    The run limit in milliseconds: the request's `run_timeout`, else the
    daemon's `run.timeout`, capped at `run.max_timeout`. The desktop app
    stops the command when it is reached.

  - **stop**: `object | null`

    Someone asked to stop the run from another client (phone, browser,
    CLI, another desktop); the desktop app running it stops it.

    - **by** (required): `string`

      Who asked: the paired device's name, or `this computer`.

    - **at** (required): `integer` format: int64, >= 0

      Unix milliseconds.

- **run_interrupted**: `integer | null` format: int64, >= 0

  Unix milliseconds when a desktop run's lease lapsed without a result
  (the app crashed or stopped responding); the request is decidable
  again. Cleared when it is run again (daemon-managed).

### 403

Remote device without decide scope, or disabled, downgraded or revoked by the time the stop commits

### 409

Not pending, or no desktop run holds the request
