# Agent: Headless

**Page:** api/agent/headless

[Download Raw Markdown](./api/agent/headless.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Agent: Headless

The headless agent endpoint drives a single, one-shot agent loop over an ephemeral, gateway-owned session. Submit a prompt (and optional target) and the gateway returns either an async job (default `text`/`json` form) or a Server-Sent Events stream (`stream-json` / `stream:true`). The endpoint is intended for automation, CI pipelines, and scripted workflows rather than interactive sessions.


Reaching this surface grants **arbitrary code execution** on the bound container. Write permission is hard-wired OFF, but `bash`/`exec` are not write-gated, and every confirm/plan/question is auto-approved. Treat the route as RCE-capable.


## Create a headless run

Submits a prompt and (optionally) a workflow to the agent. The response shape depends on the output format:

- **`text` / `json` (default)** — Async job. The endpoint returns `202` with a `job_id`; poll `GET /jobs/{id}` (and `GET /jobs/{id}/result` for the captured `Result`) for the outcome. A server-side failure surfaces as `{status:"failed", error}` in the result, not as an HTTP status on this call.
- **`stream-json` / `stream:true`** — SSE stream. The endpoint returns `200` and streams `start → result/error → end` frames over `text/event-stream`.

### `POST /api/v1/agent/headless/runs`

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `X-Hoody-Cwd` | header | string | No | Per-request working-directory scope: the `.hoody` project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd. |
| `X-Hoody-Config-Dir` | header | string | No | Per-request `--config-dir` override selecting which on-disk `.hoody` install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | header | string | No | Per-request realm selector: `"global"` or a 24-hex id (also accepted as `?realm=`). Rejected (`realm_scope_unsupported`) on active-only / no-realm routes. |
| `realm` | query | string | No | Per-request realm selector — the `in:query` alias of the `X-Hoody-Realm` header (read only when the header is absent). Same semantics. |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `prompt` | string | Yes | The prompt to drive the ephemeral session. |
| `workflow` | string | No | Optional workflow name to run instead of / alongside the prompt. |
| `model` | string | No | Optional model spec for the run. |
| `format` | string | No | Output rendering: `text` \| `json` \| `stream-json`. `stream-json` (or `stream:true`) streams the run over SSE; otherwise the run is an async job. |
| `stream` | boolean | No | Force SSE streaming (equivalent to `format:stream-json`). |
| `timeout_ms` | integer | No | Optional run timeout in milliseconds (clamped to the hard ceiling). |

#### SDK Example

```ts
await client.agent.headless.createHeadlessRun({
  prompt: "Refactor src/auth/session.ts to use the new token store and report which tests broke.",
  format: "json",
  timeout_ms: 120000
});
```

#### cURL Example

```bash
curl -X POST "https://prj_abc123-cnt_xyz789-agent-1.us-east-1.containers.hoody.icu/api/v1/agent/headless/runs" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Refactor src/auth/session.ts to use the new token store and report which tests broke.",
    "format": "json",
    "timeout_ms": 120000
  }'
```

#### Responses




Returned when `format: stream-json` or `stream: true`. The body is a `text/event-stream` of the run.

```text
event: start
data: {"run_id":"run_7f3a2b1c","model":"claude-sonnet-4.5"}

event: result
data: {"type":"text","text":"Refactored 3 files; 12 tests pass, 2 fail."}

event: end
data: {"ok":true}
```




Async job acknowledgement. Poll `GET /jobs/{id}/result` for the captured `Result` (or a `{status:"failed", error}` daemon failure).

```json
{
  "job_id": "job_01HMZ7XQ9K4D5N6P8R2T3V1WYA"
}
```




```json
{
  "code": "bad_request",
  "message": "invalid request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |




```json
{
  "code": "forbidden",
  "message": "request must arrive through the Hoody proxy"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Forbidden (not via the Hoody proxy) | The request did not reach the service through the public endpoint. | Reach the agent through `hoody-proxy` (e.g. `hoody agent …` → platform → proxy), not by connecting to the container directly. |




```json
{
  "code": "payload_too_large",
  "message": "request body exceeds the configured size limit"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `payload_too_large` | Payload too large | The request body exceeds the configured size cap (`MaxBodyBytes`). The gateway rejects an oversized body at the edge (`http.MaxBytesReader`) before the handler reads it. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |




```json
{
  "code": "rate_limited",
  "message": "request rate limit exceeded"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `rate_limited` | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the `Retry-After` header and retry; reduce the request rate. |




```json
{
  "code": "internal_error",
  "message": "internal server error"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `internal_error` | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |




```json
{
  "code": "service_unavailable",
  "message": "service unavailable"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `service_unavailable` | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor `Retry-After` and retry. |





The initial HTTP exchange on this endpoint can only return pre-dispatch statuses (`400`, `403`, `413`, `429`, `503`, `500`) plus the `200` SSE / `202` job ack. The daemon run starts **after** the response, so errors like `404` and `504` are never returned on this op. In the **async** form, a server-side failure later completes the job (visible in `GET /jobs/{id}/result` as `{status:"failed", error}`); in the **stream** form, it is delivered as the `error` SSE frame.