> ## Documentation Index
> Fetch the complete documentation index at: https://openrouter.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Shell

> Give any model a sandboxed hosted shell on the Responses and Messages APIs

export const Template = ({children, data}) => {
  const replace = s => s.replace(/\{\{(\w+)\}\}/g, (_, k) => (k in data) ? data[k] : `{{${k}}}`);
  const leafText = node => typeof node === 'string' ? node : node?.$$typeof && typeof node.props?.children === 'string' ? node.props.children : null;
  const collapseTokens = nodes => {
    const out = [];
    let i = 0;
    while (i < nodes.length) {
      const ta = leafText(nodes[i]);
      const tb = leafText(nodes[i + 1]);
      const tc = leafText(nodes[i + 2]);
      if (ta != null && tb != null && tc != null) {
        const m = (ta + tb + tc).match(/^([\s\S]*)\{\{(\w+)\}\}([\s\S]*)$/);
        if (m && (m[2] in data)) {
          out.push(m[1] + data[m[2]] + m[3]);
          i += 3;
          continue;
        }
      }
      out.push(nodes[i]);
      i++;
    }
    return out;
  };
  const process = node => {
    if (typeof node === 'string') return replace(node);
    if (Array.isArray(node)) return collapseTokens(node.map(process));
    if (node && typeof node === 'object') {
      if (node.$$typeof) return {
        ...node,
        props: process(node.props)
      };
      return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, process(v)]));
    }
    return node;
  };
  return <>{process(children)}</>;
};

export const API_KEY_REF = '<OPENROUTER_API_KEY>';

<Badge color="blue">Beta</Badge>

<Note>
  **Beta**

  Server tools are currently in beta. The API and behavior may change.

  The shell tool is available on the global endpoint (`openrouter.ai`) only.
  Requests through the
  [in-region endpoints](/docs/guides/features/in-region-routing)
  (`eu.openrouter.ai`, `us.openrouter.ai`) are rejected.
</Note>

<Warning>
  **Responses and Messages APIs only**

  The shell server tool is available through the [Responses API](/docs/api_reference/responses/overview) and the [Messages API](/docs/api/api-reference/anthropic-messages/create-a-message). Requesting it on the Chat Completions API returns a `400` error.

  The two APIs surface a shell run differently. On the Responses API the call becomes an `openrouter:shell` output item (or a native `shell_call` when you send OpenAI's tool shape). On the Messages API it becomes a `server_tool_use` content block named `openrouter:shell`, paired with an `openrouter_shell_tool_result` block carrying each command's output. Anthropic defines no native shell result block, so this OpenRouter-namespaced one is where the output arrives.
</Warning>

The `openrouter:shell` server tool gives a model a hosted shell: a sandbox-backed clone of OpenAI's hosted `shell` tool that works with any model. When the model needs to run commands, it emits a shell call; OpenRouter executes the commands server-side in an isolated Linux container and returns each command's `stdout`, `stderr`, and exit or timeout outcome.

Unlike the [Bash](/docs/guides/features/server-tools/bash) tool, the shell tool has no client-side execution mode: commands always run in a hosted environment, either OpenAI's native shell or OpenRouter's sandbox.

## How It Works

1. You include `{ "type": "openrouter:shell" }` in your `tools` array on a Responses or Messages API request. On the Responses API you can also send OpenAI's native `shell` tool shape; on non-OpenAI models it is routed to the OpenRouter sandbox automatically.
2. Based on the prompt, the model decides to run one or more shell commands and emits a shell call.
3. OpenRouter executes the commands in order, each in its own invocation, inside a sandboxed container.
4. Each command's `stdout`, `stderr`, and outcome (exit code or timeout) are returned to the model.
5. The model incorporates the results and may run further command batches in the same request.

## Quick Start

<Template
  data={{
API_KEY_REF,
MODEL: 'anthropic/claude-sonnet-4.5'
}}
>
  <CodeGroup>
    ```typescript title="TypeScript" expandable lines theme={null}
    const response = await fetch('https://openrouter.ai/api/v1/responses', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer {{API_KEY_REF}}',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: '{{MODEL}}',
        input: 'List the files in the current directory and show me the OS version.',
        tools: [
          { type: 'openrouter:shell', parameters: { engine: 'openrouter' } }
        ]
      }),
    });

    const data = await response.json();
    console.log(data);
    ```

    ```python title="Python" expandable lines theme={null}
    import requests

    response = requests.post(
      "https://openrouter.ai/api/v1/responses",
      headers={
        "Authorization": f"Bearer {{API_KEY_REF}}",
        "Content-Type": "application/json",
      },
      json={
        "model": "{{MODEL}}",
        "input": "List the files in the current directory and show me the OS version.",
        "tools": [
          {"type": "openrouter:shell", "parameters": {"engine": "openrouter"}}
        ]
      }
    )

    data = response.json()
    print(data)
    ```

    ```bash title="cURL" lines theme={null}
    curl https://openrouter.ai/api/v1/responses \
      -H "Authorization: Bearer {{API_KEY_REF}}" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "{{MODEL}}",
        "input": "List the files in the current directory and show me the OS version.",
        "tools": [
          {"type": "openrouter:shell", "parameters": {"engine": "openrouter"}}
        ]
      }'
    ```
  </CodeGroup>
</Template>

## Configuration

The shell tool accepts optional `parameters` to choose its execution engine and environment:

```json lines theme={null}
{
  "type": "openrouter:shell",
  "parameters": {
    "engine": "openrouter",
    "environment": { "type": "container_auto" }
  }
}
```

| Parameter | Type | Default | Description |
| - | - | - | - |
| `engine` | string | `auto` | Which shell engine to use: `openrouter` runs commands server-side in the OpenRouter sandbox; `auto` keeps the provider's native hosted shell when available (OpenAI) and routes to the OpenRouter sandbox on other providers |
| `environment` | object | `container_auto` | Execution environment. Use `{ "type": "container_auto" }` for an OpenRouter-managed ephemeral container, or `{ "type": "container_reference", "container_id": "..." }` to reuse an existing container. `local` environments are not supported. See [Containers](/docs/guides/features/containers) |

Containers sleep after 5 minutes idle; each command renews the timer. This is not configurable — a legacy `sleep_after_seconds` parameter is accepted and ignored. See [Container lifetime](/docs/guides/features/containers#container-lifetime).

Defaults and caps are server-enforced and may change while the tool is in beta:

| Limit | Default | Maximum |
| - | - | - |
| `timeout_ms` per command | 120,000 (2 minutes) | 300,000 (5 minutes) |
| `max_output_length` per stream, per command | 16,384 characters | 65,536 characters |
| `commands` per call | — | 100 |

A `timeout_ms` or `max_output_length` above the maximum is clamped to it. A call with more than 100 commands is rejected.

### Network Policy

Containers have **no outbound internet access by default**. The container configuration objects accept a `network_policy` field:

```json lines theme={null}
{
  "type": "openrouter:shell",
  "parameters": {
    "engine": "openrouter",
    "environment": {
      "type": "container_auto",
      "network_policy": {
        "type": "allowlist",
        "allowed_domains": ["pypi.org", "files.pythonhosted.org"]
      }
    }
  }
}
```

| Policy | Behavior |
| - | - |
| `{ "type": "disabled" }` | No outbound internet access |
| `{ "type": "allowlist", "allowed_domains": [...] }` | Outbound access restricted to hosts matching the listed hostnames or glob patterns (max 50) |
| omitted | Defaults to `disabled` — no outbound internet access. For unrestricted egress, use an allowlist of `["*"]` |

The policy is **fixed when a container starts**: sending a different `network_policy` to a warm container fails the request with a `409`. Do not try to change a running container's policy — send the same policy for the container's lifetime.

Platform constraints for allowlisted traffic:

* Only ports 80 and 443 are reachable.
* DNS resolution is provided by the platform and cannot be overridden by container configuration.
* Entries are lowercase hostnames or glob patterns — no schemes, paths, or ports. `*` matches any run of characters (`*.example.com`, `google.*.com`). An exact hostname does not cover its subdomains: `example.com` does not allow `api.example.com`; use `*.example.com` or list each hostname.
* `pip install` needs both `pypi.org` and `files.pythonhosted.org` (or `*.pythonhosted.org`) in the allowlist.

Requests to hosts outside the policy fail inside the container with a connection error (HTTP traffic sees a `520` status), which the model can read on `stderr` and react to.

### Call Arguments

The model generates the call arguments, mirroring OpenAI's hosted shell `shell_call.action`:

| Field | Type | Description |
| - | - | - |
| `commands` | string\[] | Shell commands to run, each in its own invocation, in order |
| `timeout_ms` | integer | Maximum execution time in milliseconds applied to each command |
| `max_output_length` | integer | Maximum characters returned per stream. `stdout` and `stderr` are each capped to this value, per command |

## OpenAI native shell tool

On the Responses API you can also send OpenAI's native tool shape (`{ "type": "shell" }`, or the legacy Codex `local_shell`) instead of `openrouter:shell`. On OpenAI models this uses OpenAI's own hosted shell; on any other model, OpenRouter routes the call to its sandbox transparently. The response emits the native `shell_call` output item either way.

## Response Format

The tool returns one entry per command, matching OpenAI's `shell_call_output.output[]`:

```json lines theme={null}
{
  "output": [
    {
      "stdout": "total 0\ndrwxr-xr-x 2 root root 40 Jun  1 12:00 .\n",
      "stderr": "",
      "outcome": { "type": "exit", "exit_code": 0 }
    }
  ]
}
```

Each command's `outcome` is either `{ "type": "exit", "exit_code": <int> }` or `{ "type": "timeout" }`. A non-zero exit code indicates the command failed; the error output is returned on `stderr` so the model can read and react to it.

## Output files

When a command runs in the OpenRouter sandbox and creates or changes files in the home directory (`/workspace/home`), the tool result lists them in a `files` array, next to the `container_id` the command ran in:

```json lines theme={null}
{
  "container_id": "sess_abc123",
  "files": [
    {
      "type": "container_file_citation",
      "container_id": "sess_abc123",
      "file_id": "cfile_b3V0L3JlcG9ydC5jc3Y",
      "filename": "out/report.csv",
      "start_index": 0,
      "end_index": 0
    }
  ]
}
```

* Each tool result lists at most 10 files, most recently changed first. When a call changes more than 10 files, the older ones are left out of `files` but are still saved in the container.
* `files` lists every file a command created or changed, including scratch and intermediate files. Deleted files are not listed.
* The final assistant message does not carry these files as annotations. Read them from the tool results or from the container.

To get every file in the container, call `GET /api/v1/containers/{container_id}/files` after the response completes. It lists all saved files, not just the last 10, sorted by path, and leaves out files that were deleted. The list is paginated (100 files per page by default, up to 1000 with `limit`): while `has_more` is `true`, pass the response's `last_id` as the `after` parameter to get the next page. Download a file with `GET /api/v1/containers/{container_id}/files/{file_id}/content`. Container files are deleted 30 days after the container was last used; to keep one longer, [promote it to your workspace](/docs/guides/features/containers#save-a-container-file-to-your-workspace). See [Download container files](/docs/guides/features/containers#download-container-files) for the full API.

To separate the files you want to show users from scratch files, tell the model to write its final outputs to one directory, such as `~/outputs/`, and filter the file list on that `filename` prefix. Files written outside the home directory, such as `/tmp`, are not saved or listed.

## Pricing

Sandbox time (commands run with `engine: "openrouter"`) is billed at **\$0.0001 per second**. The clock starts when a request first runs a sandbox command and stops when the response completes. A container that is idle between requests is not billed.

A request that starts a new or sleeping container is billed a minimum of 30 seconds. Later requests that reuse the same warm container pay only their metered time.

## Security

Shell execution is sandboxed by design:

* Commands execute in an isolated container, not on OpenRouter infrastructure or your machine. With `container_auto` the container is ephemeral; with `container_reference` it persists across requests.
* Containers are scoped per account and workspace, so they are never shared across tenants.
* Execution time is bounded by `timeout_ms`, clamped to 5 minutes per command.
* `stdout` and `stderr` are each truncated to `max_output_length`, a per-stream cap clamped to 65,536 characters. See [Configuration](#configuration) for the defaults.

## Next Steps

* [Server Tools Overview](/docs/guides/features/server-tools). Learn about server tools
* [Containers](/docs/guides/features/containers). How sandbox containers work
* [Bash](/docs/guides/features/server-tools/bash). Sandboxed shell for the Anthropic Messages API
* [Tool Calling](/docs/guides/features/tool-calling). Learn about user-defined tool calling
