From 01bff7bbf0511b50d18932fa2e2f13d0ff7b2501 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Tue, 11 Aug 2026 18:04:53 +0000 Subject: [PATCH] Document Patch tool input hooks and chat channel filter context Adds a "Patch tool input hooks" section covering the two configuration modes (CEL expression and static JSON), the variables a patch expression can read, the add/overwrite/remove-via-null merge rules, and two worked examples. Documents the ctx.surface and ctx.channel_id filter variables now available on the pre-tool-use and post-tool-use events, including that they are absent rather than empty for calls that didn't originate in a chat channel, and adds them to the custom function payload. Co-authored-by: c1-squire-dev[bot] --- product/admin/tool-call-hooks.mdx | 105 +++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/product/admin/tool-call-hooks.mdx b/product/admin/tool-call-hooks.mdx index 91a6a53b..fe43cc5e 100644 --- a/product/admin/tool-call-hooks.mdx +++ b/product/admin/tool-call-hooks.mdx @@ -1,8 +1,8 @@ --- title: Tool call hooks -description: Intercept MCP tool calls with built-in patterns or custom functions to redact, modify, or block calls at runtime. +description: Intercept MCP tool calls with built-in patterns, input patches, or custom functions to redact, modify, or block calls at runtime. og:title: Tool call hooks - C1 docs -og:description: Intercept MCP tool calls with built-in patterns or custom functions to redact, modify, or block calls at runtime. +og:description: Intercept MCP tool calls with built-in patterns, input patches, or custom functions to redact, modify, or block calls at runtime. --- {/* Editor Refresh: 2026-07-30 */} @@ -11,7 +11,7 @@ og:description: Intercept MCP tool calls with built-in patterns or custom functi **Activation required.** AI access management must be enabled for your tenant before you can use it. To get started, [contact the C1 support team](mailto:support@c1.ai) for a walkthrough. -Tool call hooks are interception points that run on every governed MCP tool call. They can observe a call, modify its inputs or outputs, or deny it outright. Use them to redact sensitive data, cap risky parameters, or enforce conditional access rules that see beyond the entitlement grant model. +Tool call hooks are interception points that run on every governed MCP tool call. They can observe a call, modify its inputs or outputs, or deny it outright. Use them to redact sensitive data, cap risky parameters, stamp required fields onto a call, or enforce conditional access rules that see beyond the entitlement grant model. ## How hooks work @@ -47,15 +47,15 @@ Fill out the form: | :--- | :--- | | **Name** | Required. 1–100 characters. | | **Description** | Optional. Up to 2048 characters. | -| **Hook type** | **Built-in pattern** for one of the patterns below, or **Custom function** to invoke a [function](/product/admin/functions). | -| **Event** | **Pre-tool use**, **Post-tool use**, or **Pre-output**. Some built-in patterns only support one event. | +| **Hook type** | **Built-in pattern** for one of the patterns below, **Patch tool input** to rewrite the call's arguments directly, or **Custom function** to invoke a [function](/product/admin/functions). | +| **Event** | **Pre-tool use**, **Post-tool use**, or **Pre-output**. Some built-in patterns only support one event, and **Patch tool input** only supports **Pre-tool use**. | | **Filter** | Optional CEL expression. Empty matches all calls for the event. Available variables depend on the event — see [Filter variables by event](#filter-variables-by-event). | | **Priority** | 0–1000. Lower runs first. | | **Managed by guardrails** | Off by default. See [Managed by guardrails](#managed-by-guardrails) below. | | **Enabled** | Toggle on to activate the hook immediately on save. | -If you selected **Built-in pattern**, choose the pattern and configure its options. If you selected **Custom function**, pick the function from the dropdown. +If you selected **Built-in pattern**, choose the pattern and configure its options. If you selected **Patch tool input**, enter either a CEL expression or a static JSON object. If you selected **Custom function**, pick the function from the dropdown. Click **Save**. @@ -68,13 +68,36 @@ A filter expression only sees the variables available for its event. Referencing | Event | Variables | | :--- | :--- | -| **Pre-tool use**, **Post-tool use** | `ctx.tool_name` — for example, `ctx.tool_name.startsWith("github_")` | +| **Pre-tool use**, **Post-tool use** | `ctx.tool_name` — for example, `ctx.tool_name.startsWith("github_")`. Also `ctx.surface` and `ctx.channel_id` when the call came from a chat channel — see [Chat channel context](#chat-channel-context). | | **Pre-output** | `ctx.untrusted_class`, one of the strings `"LOW"`, `"MEDIUM"`, `"HIGH"`; and `ctx.surface`, `"slack"` or `"web"`. `ctx.tool_name` is **unset** — no tool call is involved. | `ctx.untrusted_class` (a hook filter, comparing **strings**) and `ctx.untrusted_content` (a [guardrail rule](/product/admin/agent-guardrails-reference#cel-variables) variable, comparing **ordered levels**) carry the same underlying risk score under two different names and two different types. In a pre-output hook filter write `ctx.untrusted_class == "HIGH"`; in a guardrail rule write `ctx.untrusted_content == HIGH`. Using the rule form in a hook filter fails to evaluate, and the call is denied. +### Chat channel context + +On the **Pre-tool use** and **Post-tool use** events, a filter can also see where the call came from: + +| Variable | Contents | +| :--- | :--- | +| `ctx.surface` | The chat surface the call came from, such as `"slack"`. | +| `ctx.channel_id` | The ID of the chat channel the call came from, such as a Slack channel ID. | + +Both are **absent** — not empty strings — when the call didn't originate in a chat channel, as with a direct API call or a call from a connected MCP client. Check for them with `has()` before reading them, otherwise the expression fails to evaluate and the call is denied: + +```go +has(ctx.surface) && ctx.surface == "slack" +``` + +Filtering on `ctx.channel_id` scopes a hook to one team's channel while leaving the same tool untouched everywhere else: + +```go +has(ctx.channel_id) && ctx.channel_id == "C0123456789" +``` + +The same two variables are available to [patch expressions](#patch-tool-input-hooks) and are included in the payload sent to [custom function hooks](#custom-function-hooks). + ### Event and pattern must be compatible Each built-in pattern is only valid for certain events, and C1 rejects a mismatch when you save the hook. The **Event** column in [Built-in patterns](#built-in-patterns) is the authoritative list. @@ -134,6 +157,68 @@ Where a pattern takes a list, a value you configure **replaces** the default lis **A Block output hook with no surfaces selected never fires.** An empty surface list means *no* surface, not all of them — deliberately, so that forgetting to choose can't silently apply the hook everywhere. The failure mode is a hook that looks configured and enabled but withholds nothing. Select **Slack**, **Web**, or both. +## Patch tool input hooks + +A **Patch tool input** hook rewrites a tool call's input arguments directly, with no function to author or deploy. Use it to stamp a required field onto every call, overwrite a value the AI client shouldn't get to choose, or strip a field before the call reaches the MCP server. + +This hook type only runs on **Pre-tool use** — once the call has run there's no input left to patch. + +Configure it one of two ways: + +| Mode | When to use | +| :--- | :--- | +| **CEL expression** | The fields depend on the call — which channel it came from, who made it, or what the input already contains. The expression must return a JSON object. | +| **Static JSON** | The same fields apply to every call. Enter a JSON object and C1 applies it as-is. | + +### Patch expression variables + +| Variable | Contents | +| :--- | :--- | +| `ctx` | `ctx.tool_name`, plus `ctx.surface` and `ctx.channel_id` when the call came from a [chat channel](#chat-channel-context). | +| `input` | The tool call's own arguments, so the patch can read what the AI client already sent. | +| `caller` | Identity information about who made the call, such as `caller.id`. The same block described under [Pre-tool-use payload](#pre-tool-use-payload) — including the caveat that it is never an authorization input. | + +### How the patch is applied + +C1 merges the resulting object onto the tool's input one key at a time: + +- A key that isn't in the input yet is **added**. +- A key that is already in the input is **overwritten**. +- A key set to `null` is **removed** from the input. + +Keys you don't mention are left as they were. These are [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396.html) rules, the same merge behavior used by many HTTP APIs. A patch that changes the input is recorded as `MUTATED` in the [audit log](/product/admin/audit-ai-tool-usage). + +If the expression fails to evaluate or returns anything other than a JSON object, the call is denied — hooks are fail-closed. + +### Example: stamp a scope onto calls from one channel + +Say your Datadog tool calls take a `tenant_id` argument, and every call made through one Slack channel should carry that channel's scope whether or not the AI client thought to include it. + +Set the filter to match those calls: + +```go +ctx.tool_name.startsWith("datadog_") && has(ctx.channel_id) && ctx.channel_id == "C0123456789" +``` + +Then set the patch expression to: + +```go +{"tenant_id": ctx.channel_id} +``` + +Every matching call now reaches the MCP server with `tenant_id` set, and any value the client sent is replaced. + +### Example: pin a value and drop a field + +A hook that doesn't need to vary per call can use a static JSON object instead. This one forces every matching call into the development environment and removes an argument entirely: + +```json +{ + "environment": "development", + "region": null +} +``` + ## Custom function hooks When the built-in patterns don't fit, write a [function](/product/admin/functions) and attach it to a hook. C1 invokes the function with a JSON payload describing the call and uses the return value to decide whether to allow, modify, or deny. @@ -150,7 +235,9 @@ The function receives: "input": { /* the tool's input arguments */ }, "context": { "tool_source": "connector", - "classification": "WRITE" + "classification": "WRITE", + "surface": "slack", + "channel_id": "C0123456789" }, "caller": { "id": "", @@ -161,7 +248,7 @@ The function receives: } ``` -`tool_source` is `builtin`, `connector`, or `claw`. `classification` is the tool's configured action class (`READ`, `WRITE`, `DESTRUCTIVE`, `SENSITIVE`, or `DANGEROUS`). `caller` identifies who made the call; `token_id`, `mcp_client_id`, and `mcp_client_type` are omitted when empty. +`tool_source` is `builtin`, `connector`, or `claw`. `classification` is the tool's configured action class (`READ`, `WRITE`, `DESTRUCTIVE`, `SENSITIVE`, or `DANGEROUS`). `caller` identifies who made the call; `token_id`, `mcp_client_id`, and `mcp_client_type` are omitted when empty. `surface` and `channel_id` describe the [chat channel](#chat-channel-context) the call came from and are omitted when it didn't come from one. `caller` is for logging and call-shape decisions only — it is **never an authorization input**, because the grant decision is made before the hook runs. Identity-based scope belongs in [access profiles](/product/admin/tools-and-toolsets), not hooks.