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

# Per-Tool Guardrails (TypeScript)

> Guard a single tool's arguments and results without hand-wrapping the function

Guard a single tool's arguments and results without hand-wrapping the function — declare `inputGuardrails` / `outputGuardrails` right on the tool, next to `approval` and `restartSafe`.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, tool } from 'praisonai';

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [(args) =>
    String(args.to ?? '').endsWith('@corp.com')
      ? [true, args]
      : [false, 'Recipient is outside the company domain.']],
  execute: async ({ to, body }) => `sent to ${to}`,
});

const agent = new Agent({
  instructions: 'Send emails on request.',
  tools: [sendEmail],
});

await agent.start('Email alice@gmail.com the release notes');
// -> the call is blocked; the reason is fed back to the model, which can adapt.
```

The guardrail fires on every invocation of `send_email` and never for any other tool.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    LLM[LLM tool call] --> In{input guardrail}
    In -->|block| Back1[reason fed to model]
    In -->|allow / rewrite| Run[tool runs]
    Run --> Out{output guardrail}
    Out -->|block| Back2[reason fed to model]
    Out -->|allow / substitute| LLM2[back to LLM]

    classDef node fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef guard fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef back fill:#6366F1,stroke:#7C90A0,color:#fff

    class LLM,LLM2 node
    class In,Out guard
    class Run tool
    class Back1,Back2 back
```

## Quick Start

**1. Block a tool's arguments by rule**

An input guardrail returns `[false, 'reason']` to block. The reason is handed back to the model as the tool result — never thrown at the user.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, tool } from 'praisonai';

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [(args) =>
    String(args.to ?? '').endsWith('@corp.com')
      ? [true, args]
      : [false, 'Recipient is outside the company domain.']],
  execute: async ({ to, body }) => `sent to ${to}`,
});

const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
await agent.start('Email bob@corp.com the agenda');   // allowed
```

**2. Rewrite the arguments before the tool runs**

Return `[true, newArgs]` to rewrite. The rewritten object is what the tool receives.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, tool } from 'praisonai';

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [(args) => [true, { ...args, bcc: 'audit@corp.com' }]],
  execute: async ({ to, body, bcc }) => `sent to ${to}, bcc ${bcc}`,
});

const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
await agent.start('Email dana@corp.com the report');
```

> [!WARNING]
>
> An input guardrail's rewrite must stay an **object of keyword arguments**. Returning anything else (array, string, `null`) is a contract violation and blocks the call, because handing a non-object to the tool would throw inside your code instead of producing a message the model can act on.

**3. Substitute the result**

An output guardrail sees the raw result before it re-enters the LLM context. Return `[true, newResult]` to substitute, or `[false, 'reason']` to block.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, tool } from 'praisonai';

const readConfig = tool({
  name: 'read_config',
  description: 'Read the service config',
  outputGuardrails: [(result) => [true, String(result).replace('sk-live-1234', '[REDACTED]')]],
  execute: async () => 'STRIPE_KEY=sk-live-1234',
});

const agent = new Agent({ instructions: 'Report config values.', tools: [readConfig] });
await agent.start('What is the Stripe key?');
// -> the model sees STRIPE_KEY=[REDACTED]
```

**4. Chain several guardrails**

Pass an array — each guardrail's allowed value feeds the next; the chain short-circuits on the first failure.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, tool } from 'praisonai';

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [
    (args) => String(args.to ?? '').endsWith('@corp.com')
      ? [true, args]
      : [false, 'off-domain recipient'],
    (args) => [true, { ...args, bcc: 'audit@corp.com' }],
  ],
  execute: async (args) => `sent to ${args.to}, bcc ${args.bcc}`,
});

const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
await agent.start('Email carol@corp.com the notes');
```

***

## Verdict shapes accepted

A guardrail's return value tells the runtime whether to allow, rewrite, or block.

| Return value | Meaning |
| - | - |
| `[true, value]` | Allow; `value` replaces args/result (rewrite / substitute). |
| `[true, null]` / `[true, undefined]` | Allow **unchanged** — does NOT replace with null. |
| `[false, 'reason']` | Block; reason goes back to the model. |
| `true` / `false` | Bare boolean — allow unchanged, or block with default reason. |
| `GuardrailValidationResult` (`{ success, result?, error? }`) | Same rules using the existing shape. |
| `null` / `undefined` | "No opinion" — allow unchanged. |
| Anything else | **Fails closed** — an unreadable verdict is not an approval. |
| Throws | **Fails closed** unless the chain's `failOpen` is set. |

***

## How per-tool guardrails run

Per-tool guardrails are the layer closest to the tool in both directions. The input guardrail runs **last** on the way in (immediately before dispatch); the output guardrail runs **first** on the way out (before any agent-wide handling).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant LLM
    participant Approval as Human approval gate
    participant AgentGR as Agent-wide guardrail / policy
    participant ToolIn as Per-tool input guardrail
    participant Tool
    participant ToolOut as Per-tool output guardrail
    participant Fence as Trust fence

    LLM->>Approval: proposed tool call
    Approval->>AgentGR: approved args
    AgentGR->>ToolIn: (possibly rewritten) args
    Note over ToolIn: runs LAST in — nothing can<br/>rewrite args after its verdict
    ToolIn->>Tool: allowed / rewritten args
    Tool-->>ToolOut: raw result
    Note over ToolOut: runs FIRST out — sees the<br/>RAW result before the fence
    ToolOut->>Fence: allowed / substituted result
    Fence-->>LLM: fenced result
```

> [!NOTE]
>
> The order inside `FunctionTool.execute` is `restartSafe` → approval → **input guardrail** → tool → **output guardrail**. Human approval stays **upstream** of every automated rewrite, so a reviewer audits exactly what the model proposed. The input guardrail runs immediately before dispatch, so "the tool never runs with arguments its own guardrail did not see" holds.

***

## Detecting a denial

A blocked call returns a `ToolGuardrailDenial` object rather than throwing.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { isToolGuardrailDenial } from 'praisonai';

const result = await sendEmail.execute({ to: 'alice@gmail.com', body: 'hi' });
if (isToolGuardrailDenial(result)) {
  console.log(result.error);            // "Tool 'send_email' was blocked by an input guardrail: ..."
  console.log(result.tool_guardrail);   // 'input' | 'output'
}
```

The model receives this same object as the tool result, tagged `guardrail_denied: true`, so it can pick different arguments, choose another tool, or explain to the user — never a stack trace.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
interface ToolGuardrailDenial {
  error: string;                    // "Tool 'X' was blocked by an input guardrail: <reason>"
  guardrail_denied: true;
  tool_guardrail: 'input' | 'output';
}
```

***

## Reusing an existing guardrail object

Any object exposing `validateToolCall(toolName, args)` or `validateToolResult(toolName, result)` — including an existing agent-wide `ToolGuardrailChain` — drops in as a single entry.

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, tool, ToolInputGuardrail } from 'praisonai';

const domainCheck = new ToolInputGuardrail(
  (args) => String(args.to ?? '').endsWith('@corp.com')
    ? [true, args]
    : [false, 'off-domain'],
  'domain_check',
);

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [domainCheck],
  execute: async ({ to }) => `sent to ${to}`,
});

const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
await agent.start('Email erin@corp.com the plan');
```

> [!NOTE]
>
> `direction` is passed through the base `CallableToolGuardrail` constructor rather than declared as a subclass `readonly direction = INPUT` field, because subclass field initializers run **after** `super()` and would leave the base constructor seeing `undefined`. You only hit this when subclassing `CallableToolGuardrail` directly.

***

## Per-tool vs agent-wide guardrails

Per-tool guardrails share the same machinery as agent-wide ones — a guardrail written for one scope drops into the other.

| | Agent-wide `Agent({ guardrails })` | Per-tool `inputGuardrails` / `outputGuardrails` |
| - | - | - |
| Fires for | Every tool call | Just this tool |
| Configured on | `Agent({...})` | The tool itself, next to `approval` and `restartSafe` |
| Blocks by | Returning `[false, ...]` | Same |
| On block | Reason fed back to the model | Same |
| Rewrite / substitute | `[true, value]` | Same |

***

## Best Practices

> **Scope the guardrail to where the rule lives**
>
> When a rule belongs to one tool — "`send_email` recipients must be on-domain" — declare it on that tool, not agent-wide. An agent-wide guardrail fires for every tool and has to branch on `toolName`.

> **Prefer rewriting arguments over blocking**
>
> Returning `[true, sanitisedArgs]` is safer than `[false, reason]` and hoping the model retries correctly. Fix the call rather than bouncing it back.

> **Redact in the output guardrail rather than reject**
>
> When a result carries a secret, return `[true, redacted]` so the model still gets a usable answer, instead of `[false, ...]` which throws the whole result away.

> **Keep guardrail logic pure**
>
> A guardrail should inspect and transform its input with no side effects. A guardrail that throws fails closed and blocks the tool, unless the chain's `failOpen` is set.

***

## Related

> **[Guardrails](https://praison.ai/docs/js/guardrails.md)**
>
> Agent-wide input and output guardrails.

> **[Approval](https://praison.ai/docs/js/approval.md)**
>
> Human-in-the-loop sign-off before a tool runs.

> **[Per-Tool Guardrails (Python)](https://praison.ai/docs/features/per-tool-guardrails.md)**
>
> The same feature in the Python SDK.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.