# Approval Settings

> Human-in-the-loop (HIL) for tool execution.

**URL:** https://inference.sh/docs/api/agent/approval
**Last updated:** 2026-10-11

---

Human-in-the-loop (HIL) for tool execution.

---

## Overview

Require user approval before tools execute:

<div data-type="codegroup">

```python Python
from inferencesh import tool, string

delete_file = (
    tool("delete_file")
    .describe("Delete a file from the system")
    .param("path", string("File path"))
    .require_approval()  # ← Requires approval
    .handler(lambda args: delete(args["path"]))
    .build()
)
```

```typescript JavaScript
import { tool, string } from '@inferencesh/sdk';

const deleteFile = tool('delete_file')
  .describe('Delete a file from the system')
  .param('path', string('File path'))
  .requireApproval()
  .handler(args => deleteFunc(args.path))
  .build();
```

</div>

---

## How It Works

1. Agent decides to call tool with `requireApproval`
2. Execution pauses with status `awaiting_approval`
3. User sees the tool call and can approve or reject
4. If approved: tool executes normally
5. If rejected: agent receives rejection and can try alternatives

---

## Approval Flow

```
Agent: "I need to delete old-file.txt"
       [calls delete_file(path="old-file.txt")]

System: ⏸ Awaiting approval
        Tool: delete_file
        Args: {"path": "old-file.txt"}
        [Approve] [Reject]

User: [clicks Approve]

Tool: Executes delete("old-file.txt")

Agent: "Done! I've deleted old-file.txt"
```

---

## Any Tool Type

All tool types support approval:

<div data-type="codegroup">

```python Python
# Client Tools
tool("dangerous_action").require_approval().handler(do_dangerous).build()

# App Tools
app_tool("expensive_model", "expensive/model@latest").require_approval().build()

# Agent Tools
agent_tool("expert", "my-team/expert@latest").require_approval().build()

# Webhook Tools
webhook_tool("external_api", "https://api.example.com").require_approval().build()
```

```typescript JavaScript
// Client Tools
tool('dangerous_action').requireApproval().handler(doDangerous).build()

// App Tools
appTool('expensive_model', 'expensive/model@latest').requireApproval().build()

// Agent Tools
agentTool('expert', 'my-team/expert@latest').requireApproval().build()

// Webhook Tools
webhookTool('external_api', 'https://api.example.com').requireApproval().build()
```

</div>

---

## Use Cases

### Destructive Operations

<div data-type="codegroup">

```python Python
delete_tool = (
    tool("delete")
    .describe("Delete data")
    .require_approval()
    .build()
)
```

```typescript JavaScript
const deleteTool = tool('delete')
  .describe('Delete data')
  .requireApproval()
  .build();
```

</div>

### External Integrations

<div data-type="codegroup">

```python Python
send_email = (
    webhook_tool("send_email", "https://api.email.com/send")
    .describe("Send an email")
    .require_approval()  # Don't spam!
    .build()
)
```

```typescript JavaScript
const sendEmail = webhookTool('send_email', 'https://api.email.com/send')
  .describe('Send an email')
  .requireApproval()
  .build();
```

</div>

### Expensive Operations

<div data-type="codegroup">

```python Python
premium_analysis = (
    app_tool("analyze", "expensive/analyzer@latest")
    .describe("Premium analysis ($5/call)")
    .require_approval()
    .build()
)
```

```typescript JavaScript
const premiumAnalysis = appTool('analyze', 'expensive/analyzer@latest')
  .describe('Premium analysis ($5/call)')
  .requireApproval()
  .build();
```

</div>

---

## Agent template defaults

Saved agent versions (`AgentVersionDTO` / `version` on `POST /agents`) may include a `permissions` object. Today the only field is `allow_all_tools`.

When `permissions.allow_all_tools` is `true`, **each new chat in the agent's home workspace** starts with [allow all tools](#per-chat-permissions) enabled (`agent_data.allow_all_tools` and the `allow_all_tools` chat rule). Chats another workspace starts with a published agent do **not** copy this default — they run on that workspace's remotes and tools, and the agent author cannot disable approvals there. The chat owns the setting from then on — toggling it in [chat settings](/docs/agents/chatting#chat-settings) or via `POST /chats/:id/settings` only affects that conversation; redeploying the agent does not change existing chats.

Use this for agents that run unattended (webhook, cron, or trigger dispatches) where pausing on every tool would stall the run. Team and org policy layers can still **ask** or **deny** over the chat default.

Set defaults in the agent template (REST `version.permissions`, SDK types, or top-level `permissions` in `belt agent deploy` YAML). See [Create or update agent template](/docs/api/rest/agents#create-or-update-agent-template).

<div data-type="codegroup">

```yaml
# agent.yaml (belt agent deploy)
name: oncall-triage
permissions:
  allow_all_tools: true
```

```json
{
  "name": "oncall-triage",
  "version": {
    "system_prompt": "Triage incoming alerts.",
    "core_app": { "ref": "openrouter/claude-sonnet-46" },
    "permissions": { "allow_all_tools": true }
  }
}
```

</div>

Types: `AgentPermissions` in the [Python](/docs/api/sdk-python) and [JavaScript](/docs/api/sdk-javascript) SDKs.

---

## Per-chat permissions

Approvals are scoped to each chat through **policy rules**. The workspace **chat settings → Permissions** pane lists every layer in force — chat, agent, user, team, and org — most specific first.

- **Always allow** on one pending tool approves that call and adds an **allow** rule on the chat layer (loop `Tool` or harness `Harness`, depending on how the chat runs). Each choice is labeled in plain language (for example `npm run commands on laptop`); the same `label` appears on the rule in **chat settings → Permissions**. See [Always allow tool](/docs/api/rest/agents#always-allow-tool).
- **Allow all tools** in [chat settings](/docs/agents/chatting#chat-settings) (or `allow_all_tools` on `POST /chats/:id/settings`) runs every tool without asking and approves anything already waiting. It appears as the `allow_all_tools` rule on the chat layer; turn it off to require approval again for tools not covered by other allow rules.
- **Add or revoke chat rules** with `POST` / `DELETE` on [`/chats/:id/rules`](/docs/api/rest/agents#chat-permissions-rules). Chat rules can only narrow what layers above allow. Test a call with `POST /chats/:id/rules/explain`.

Governance rules from a team or org are read-only in chat settings; they can loosen what a stricter layer above would require, while non-governance layers only narrow.

Per-chat changes do not update the agent version's `permissions` object — they only affect the current conversation (plus shared policy layers attached to the agent, user, team, or org). New chats in the agent's home workspace pick up the agent version defaults again unless you change the template.

**Published agents:** **Allow** rules on an agent template apply only in that agent's home workspace. In another workspace's chat with the same agent, **ask** and **deny** rules on the template still apply, but **allow** rules do not — the caller's remotes and tools are never opened by the publisher's defaults. Chat-level **allow** rules the caller adds still apply.

---

## Scoped always allow and explain

For a single invocation in `awaiting_approval`, **always allow** can save a **narrow** chat rule instead of approving only this once:

1. `GET /chats/:chatId/tools/:toolInvocationId/always-allow/options` — choices ordered narrowest first (exact `remote_exec` command, command prefix, every command on the remote, whole tool, …). Each option includes a stable `key`, human `label`, `scope`, and the `PolicyRuleDTO` rows it would write. When nothing fits, `options` is empty and `unavailable` explains why.
2. `POST /chats/:chatId/tools/:toolInvocationId/always-allow` with `{ "option": "<key>" }` — saves those rules and executes the pending call. Omit `option` for the legacy default (exact command pieces for `remote_exec`, whole-tool allow otherwise). **409** means the key is stale (re-list options); **400** means the call is no longer waiting.
3. `POST /chats/:chatId/tools/:toolInvocationId/explain` — on-demand plain-language summary (`risk_level`, `explanation`, `reasoning`, `risk`). The model runs once per invocation and the API caches the result. Not available on harness-backed chats.

**HTTP tools:** The API resolves the call URL to a host the same way the HTTP tool does. When the pending approval is about that **host** (`WebFetch(domain:…)` — from governance or the default ask), chat rules cannot answer it, so **always allow** lists **no** options and `unavailable` names the host; posting without an `option` does **not** save a whole-tool allow (that would not stop the host question and would leave a tool-wide allow behind). When only the tool's own approval gate asked, the usual whole-tool option is offered.

→ [Agents REST API — List always-allow options](/docs/api/rest/agents#list-always-allow-options) · [Save always-allow](/docs/api/rest/agents#save-always-allow) · [Explain tool call](/docs/api/rest/agents#explain-tool-call)

**JavaScript Agent SDK** (**@inferencesh/sdk ≥ v0.16.0**): `useAgentActions().getAlwaysAllowOptions`, `alwaysAllowTool(id, { option })`, and `explainTool(id)`. See [JavaScript SDK — Scoped always allow and explain](/docs/api/sdk-javascript#scoped-always-allow-and-explain).

---

## Terminal workflow

When an agent pauses for approval, use the CLI from any machine with your API key:

```bash
belt chat pending                    # list invocations awaiting approval
belt chat pending ti_abc123          # inspect one invocation (tool, args, agent)
belt chat approve ti_abc123          # approve and execute
belt chat approve ti_abc123 --watch  # approve, then stream the agent's reply
belt chat reject ti_abc123 --reason "not authorized"
```

REST equivalents: `POST /tools/:invocation_id/invoke` and `POST /tools/:invocation_id/reject`. See [Agents API](/docs/api/rest/agents#approve-tool-hil).

You can also resolve approval gates through the generic [interrupt API](/docs/api/rest/agents#interrupt-gates): list pending interrupts on the active run (`GET /agent-runs/:runId/interrupts`) and call `POST /interrupts/:id/resolve` with `{"decision": "allow"}` or `"deny"`.

From the CLI, use `belt interrupt list <run-id>` and `belt interrupt resolve <interrupt-id> allow|deny` — see [CLI setup — Interrupt](/docs/extend/cli-setup#interrupt-cli).

→ [CLI setup](/docs/extend/cli-setup#chat-cli)

---

## Handling Rejections

When a user rejects a tool call, the agent receives feedback and can try alternatives:

```
Agent: "Let me delete that file"
       [calls delete_file → REJECTED]

Agent: "I understand you don't want to delete it. 
        Would you like me to archive it instead?"
```

---

## Programmatic guardrails

For server-side policy checks (allow/deny based on tool name, arguments, or external rules), use [lifecycle hooks](/docs/api/agent/lifecycle-hooks) on the `agent.tool_call` event. A synchronous webhook can return `{"decision": "deny", "reason": "..."}` to skip execution without pausing for human review.

---

## Selective Approval

Mix approved and auto-execute tools:

<div data-type="codegroup">

```python Python
agent = client.agent({
    "core_app": { "ref": "infsh/claude-sonnet-4@latest" },
    "tools": [
        # Auto-execute (safe)
        tool("read_file").handler(read_file).build(),
        tool("list_files").handler(list_files).build(),
        
        # Require approval (dangerous)
        tool("delete_file").require_approval().handler(delete_file).build(),
        tool("modify_file").require_approval().handler(modify_file).build(),
    ]
})
```

```typescript JavaScript
const agent = client.agent({
  core_app: { ref: 'infsh/claude-sonnet-4@latest' },
  tools: [
    // Auto-execute (safe)
    tool('read_file').handler(readFile).build(),
    tool('list_files').handler(listFiles).build(),
    
    // Require approval (dangerous)
    tool('delete_file').requireApproval().handler(deleteFile).build(),
    tool('modify_file').requireApproval().handler(modifyFile).build(),
  ]
});
```

</div>
