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

# Tool Policy Hooks (Inbound `/v1/hooks/pretool`)

> Enforce tool policies on agent tool calls before execution. Inbound endpoint — your agent calls the gateway, not the other way around.

## Overview

The Tool Policy Hooks endpoint lets you enforce your organization's tool policies on agent tool calls. Before your agent executes a tool — whether it's a terminal command, an MCP tool, or any other action — your agent sends an **inbound** request to this endpoint and the gateway returns a decision. This is a request the agent makes to Unbound; Unbound does not call out to your agent.

Any agent framework or custom application can integrate with it.

Policies are configured at [gateway.getunbound.ai/policies/tool-policies](https://gateway.getunbound.ai/policies/tool-policies).

## Endpoint

```http theme={null}
POST /v1/hooks/pretool
```

## Authentication

Include your API key in the Authorization header:

```
Authorization: Bearer YOUR_API_KEY
```

## Request Body

| Parameter           | Type   | Required | Description                                                                                |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------ |
| `conversation_id`   | string | Yes      | Unique identifier for the conversation or session                                          |
| `model`             | string | Yes      | The model being used                                                                       |
| `event_name`        | string | Yes      | `pre_tool_use` for tool policy checks, `user_prompt` for prompt guardrails                 |
| `unbound_app_label` | string | Yes      | Your application identifier (e.g., `openclaw`, `claude-code`, `cursor`, or a custom label) |
| `pre_tool_use_data` | object | Yes      | Details about the tool being invoked (see below)                                           |
| `messages`          | array  | No       | Conversation messages for context                                                          |

### pre\_tool\_use\_data Object

| Parameter   | Type   | Required    | Description                                                                                                                                 |
| ----------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `tool_name` | string | Yes         | Name of the tool. Use `Bash`, `Shell`, or `exec` for terminal commands. For MCP tools, use the full name (e.g., `mcp__slack__send_message`) |
| `command`   | string | Conditional | The shell command string. Required when `tool_name` is `Bash`, `Shell`, or `exec`                                                           |
| `metadata`  | object | No          | Additional metadata. For MCP tools, include `mcp_server` and `mcp_tool`                                                                     |

## Response

| Field                         | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decision`                    | string | `allow`, `deny`, `ask`, or `approval_required`                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `reason`                      | string | Present when `decision` is `deny` or `ask`. Human-readable explanation                                                                                                                                                                                                                                                                                                                                                                                            |
| `approvalCheck`               | object | Present when `decision` is `approval_required`. Carries the `requestId` you poll with **and the `policyIds`** of the matching Require-Slack-Approval policies — send both back when polling (see [Checking approval status](#checking-approval-status)). When the request was escalated to an admin it also carries `escalatedToAdmin` and `escalatedAdminContact` so you can tell the user who to chase                                                          |
| `policy_check_failure_action` | string | Carries the action your organization has configured for the case where the policy check itself fails: `allow` (fail-open) or `block` (fail-closed). `allow`, `deny` and `ask` decisions all carry it, and so does a **503**. It is **absent** only from the `200 OK` fail-open response to a malformed or unauthenticated request. Honor it when the client cannot reach the gateway, and cache the last value you saw — see [Error responses](#error-responses). |
| `additionalContext`           | string | Present on `deny` and `ask` responses when the matching policy supplies extra guidance for the client — a human-readable remediation hint or context string to surface alongside the approval prompt. Optional — absent when the policy has no extra context to add.                                                                                                                                                                                              |

### Decision Values

| Decision            | Meaning                                                                                       | Recommended Action                                                                                                                   |
| ------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `allow`             | Tool call is permitted                                                                        | Proceed with execution                                                                                                               |
| `deny`              | Tool call is blocked by policy                                                                | Do not execute. Show the `reason` to the user                                                                                        |
| `ask`               | Tool call requires user confirmation                                                          | Prompt the user for approval before executing. If your agent has no confirmation UI, treat as `allow` based on your security posture |
| `approval_required` | Tool call matched a Require Slack Approval policy. An approval request has been sent to Slack | Do not execute yet. Hold the call and poll [the approval-status endpoint](#checking-approval-status) until the approver decides      |

## Checking approval status

When a tool call comes back as `approval_required`, an approver has been asked to allow or deny it in Slack. Hold the call and poll this endpoint until they answer.

```
POST https://api.getunbound.ai/v1/hooks/pretool/approval-status
```

Authenticate exactly as you do for the hook itself — `Authorization: Bearer <UNBOUND_API_KEY>`.

### Request

Send back both values you were given in `approvalCheck` — the `requestId` and the `policyIds`:

```json theme={null}
{
  "requestId": "<approvalCheck.requestId>",
  "policyIds": ["<policy-id-1>", "<policy-id-2>"]
}
```

| Field       | Type      | Required    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ----------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `requestId` | string    | Yes         | The `requestId` from the `approvalCheck` object on the `approval_required` response                                                                                                                                                                                                                                                                                                                                                                          |
| `policyIds` | string\[] | Recommended | The `policyIds` from the same `approvalCheck` object. With `requestId` alone, polling resolves the decision an approver made for **this specific request**. Passing `policyIds` too also catches a **blanket "allow for an hour" approval** an approver may have granted across the whole policy — omit them and that blanket approval isn't seen, so the call keeps returning `pending` until this request is answered directly or you hit your own timeout |

### Response

```json theme={null}
{ "decision": "pending" }
```

| Decision  | Meaning                 | What to do                                  |
| --------- | ----------------------- | ------------------------------------------- |
| `pending` | Nobody has answered yet | Keep holding, and poll again                |
| `allow`   | An approver allowed it  | Execute the tool call                       |
| `deny`    | An approver denied it   | Do not execute. Tell the user it was denied |

<Note>
  `pending` is the safe default. If the request can't be resolved — a missing `requestId`, or a transient failure — the endpoint answers `pending` rather than guessing, so a held tool call is never released by accident.

  That means **`pending` is not self-terminating.** Decide your own limit — a maximum number of polls, or a deadline — and choose what happens when you hit it. Treating a timeout as `deny` fails closed; treating it as `allow` fails open. Pick the one that matches your security posture, and surface the wait to your user so the agent doesn't just look hung.
</Note>

## How It Works

The endpoint handles two types of tools differently:

**Terminal commands** (`Bash`, `Shell`, `exec`): The command is automatically classified and matched against your configured terminal command policies.

**MCP tools**: The `mcp_server` and `mcp_tool` from metadata are matched against your MCP tool policies.

**Other tools**: Tools that don't match a terminal command or MCP tool pattern return `allow`.

## Examples

### Terminal Command (Blocked)

```bash theme={null}
curl -X POST 'https://api.getunbound.ai/v1/hooks/pretool' \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "session-123",
    "model": "claude-sonnet-4-6",
    "event_name": "pre_tool_use",
    "unbound_app_label": "my-agent",
    "pre_tool_use_data": {
      "tool_name": "Bash",
      "command": "rm -rf /tmp/data",
      "metadata": {}
    },
    "messages": [
      {"role": "user", "content": "clean up temp files"}
    ]
  }'
```

**Response:**

```json theme={null}
{
  "decision": "deny",
  "reason": "This command is blocked by your organization's policy.",
  "policy_check_failure_action": "block"
}
```

### Terminal Command (Allowed)

```bash theme={null}
curl -X POST 'https://api.getunbound.ai/v1/hooks/pretool' \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "session-123",
    "model": "claude-sonnet-4-6",
    "event_name": "pre_tool_use",
    "unbound_app_label": "my-agent",
    "pre_tool_use_data": {
      "tool_name": "Bash",
      "command": "ls -la /tmp",
      "metadata": {}
    },
    "messages": []
  }'
```

**Response:**

```json theme={null}
{
  "decision": "allow",
  "policy_check_failure_action": "allow"
}
```

### MCP Tool

```bash theme={null}
curl -X POST 'https://api.getunbound.ai/v1/hooks/pretool' \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "session-123",
    "model": "claude-sonnet-4-6",
    "event_name": "pre_tool_use",
    "unbound_app_label": "my-agent",
    "pre_tool_use_data": {
      "tool_name": "mcp__slack__send_message",
      "command": "",
      "metadata": {
        "mcp_server": "slack",
        "mcp_tool": "send_message"
      }
    },
    "messages": []
  }'
```

**Response** (depends on your configured policies):

```json theme={null}
{
  "decision": "deny",
  "reason": "This MCP tool call is blocked by your organization's policy.",
  "policy_check_failure_action": "block"
}
```

## Error responses

The endpoint returns `200 OK` with `decision: "allow"` for requests that are malformed or unauthenticated — a fail-open posture. The response carries the decision **and nothing else**: `policy_check_failure_action` is absent, not set to a default.

```json theme={null}
{
  "decision": "allow"
}
```

When policy evaluation itself fails, the endpoint returns **503**:

```json theme={null}
{
  "error": "policy engine unavailable — please retry",
  "policy_check_failure_action": "block"
}
```

A 503 **does** carry `policy_check_failure_action`, taken from your organization's configuration. Use it. The field is omitted only when the gateway could not resolve your configuration at all — in that case fall back to the last value you cached from a successful response.

If your client needs fail-closed behavior, gate it locally before calling the endpoint (validate the request body, confirm the API key is present) rather than relying on the gateway to reject.

<Warning>
  **Treat an absent `policy_check_failure_action` as "no new information", never as a value.** The `200 OK` fail-open response omits the field entirely — it does not carry a default. A client that reads the omission as fail-open (for example, right after an API-key rotation) would silently apply fail-open behaviour on a fail-closed org. Keep the last value you received as authoritative, and never infer one from its absence.
</Warning>

<Note>
  This is the current behaviour, not an aspirational contract. Two more things worth knowing:

  `/approval-status` is **fail-closed**: a missing or invalid API key returns **401**, the opposite of `/pretool`. The "pending is the safe default" guidance applies only after authentication succeeds.

  Ignore response fields you don't recognise — new ones are added over time, the same rule that already applies to unknown `decision` values.
</Note>

## Integration Guide

To integrate tool policies into your agent framework:

1. **Before each tool execution**, send a `POST` request to `/v1/hooks/pretool` with the tool details
2. **Check the `decision`** in the response:
   * `allow` — execute the tool normally
   * `deny` — block execution and surface the `reason` to the user
   * `ask` — prompt the user for confirmation, or treat as `allow` if your agent has no interactive UI
   * `approval_required` — do **not** execute. Hold the call and poll [the approval-status endpoint](#checking-approval-status) until the approver decides. See the full contract in [Decision Values](#decision-values)
3. **Treat an unrecognized `decision` as `deny`.** New decision values can be added over time; a client that falls through to "execute" on a value it doesn't know can bypass a guardrail. Default the unknown case to blocking, not allowing
4. **Handle errors gracefully** — if the endpoint is unreachable, decide whether to fail open (allow) or fail closed (deny) based on your security requirements
5. **Configure policies** at [gateway.getunbound.ai/policies/tool-policies](https://gateway.getunbound.ai/policies/tool-policies)
