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

# Guardrails using Claude Code Hooks

> Validate and mutate Claude Code prompts and tool calls by running TrueFoundry guardrails through Claude Code hooks.

Claude Code routes model traffic through the gateway via `ANTHROPIC_BASE_URL`, but coding assistants also perform work the model API never sees — running shell commands, editing files, and calling MCP tools. TrueFoundry governs that surface through **[Claude Code hooks](https://code.claude.com/docs/en/hooks)**: each hook payload is sent to a dedicated gateway endpoint that runs your configured guardrails and returns Claude Code's native hook response, so a violation **blocks** the action (or **rewrites** it) directly in the client.

<Note>
  This is complementary to the standard [Claude Code integration](/docs/ai-gateway/claude-code). That covers model requests (cost, routing, LLM guardrails on completions); this page covers **prompt submissions and tool calls** via hooks. You can use either or both.
</Note>

## How it works

1. Claude Code fires a hook (e.g. before a tool runs) and sends the event payload on stdin.
2. A hook command `POST`s that payload to the gateway endpoint `POST /hooks/claude-code`.
3. The gateway runs the guardrails you nominated for that hook and returns a JSON response containing Claude Code's native hook fields (`decision`, `permissionDecision`, `updatedInput`, `updatedToolOutput`).
4. Claude Code acts on that response — allowing, blocking, or applying the rewritten content.

## What you can and can't do

There are two things a guardrail can do at a hook:

* **Validation** — decide **block or allow**. A block stops the prompt or tool call (or, for `PostToolUse`, replaces the already-produced result — see caveats).
* **Mutation** — **rewrite the content** in place (e.g. redact an email from a tool's input or output) and let the action continue with the sanitized version.

The three supported hook events differ in which of these they support:

| Hook event         | Scans                                |                                   Validation (block)                                  |               Mutation (rewrite)              |
| ------------------ | ------------------------------------ | :-----------------------------------------------------------------------------------: | :-------------------------------------------: |
| `UserPromptSubmit` | The user's prompt                    |                       ✅ blocks the prompt (`decision: "block"`)                       | ❌ not supported — a prompt can't be rewritten |
| `PreToolUse`       | Tool **input**, before the tool runs |                    ✅ denies the call (`permissionDecision: "deny"`)                   |     ✅ rewrites the input (`updatedInput`)     |
| `PostToolUse`      | Tool **result**, after the tool ran  | ⚠️ can't stop the tool (already ran) — replaces the result with the guardrail message |  ✅ rewrites the result (`updatedToolOutput`)  |

In short:

* **Validation works on all three events.**
* **Mutation works only on the MCP tool paths** — `PreToolUse` (tool input) and `PostToolUse` (tool result). The prompt path (`UserPromptSubmit`) is **validation-only**.

<Note>
  There is no LLM-**output** direction here: Claude Code has no hook for the assistant's generated response. To guardrail model *responses*, use output guardrails on the model request path (see [Guardrails](/docs/ai-gateway/guardrails-getting-started)).
</Note>

## Choosing which guardrails run

Guardrails for the hook endpoint are selected **per request** via the `x-tfy-guardrails` header (the same JSON format used elsewhere — see [Applying guardrails via header](/docs/ai-gateway/guardrails-getting-started)). The endpoint does not read tenant Guardrail Config rules; the caller nominates the selectors.

```json theme={"dark"}
{
  "llm_input_guardrails": ["group/guardrail-name"],
  "mcp_tool_pre_invoke_guardrails": ["group/guardrail-name"],
  "mcp_tool_post_invoke_guardrails": ["group/guardrail-name"]
}
```

| Field                                          | Applies to         |
| ---------------------------------------------- | ------------------ |
| `llm_input_guardrails` (or `input_guardrails`) | `UserPromptSubmit` |
| `mcp_tool_pre_invoke_guardrails`               | `PreToolUse`       |
| `mcp_tool_post_invoke_guardrails`              | `PostToolUse`      |

Each selector is `group/guardrail-name`. If no selector is nominated for a hook's direction, that request passes through.

## Prerequisites

1. A guardrail **group** and at least one **guardrail integration** — see [Create a guardrail](/docs/ai-gateway/guardrails-getting-started). Note the selector string, e.g. `security/email-regex`.
2. A TrueFoundry API key with access to that guardrail group.
3. Claude Code installed and (optionally) already pointed at the gateway per the [Claude Code integration](/docs/ai-gateway/claude-code).

## Configure the hooks

Add the hooks to your project's `.claude/settings.json` (or `~/.claude/settings.json`). This example wires all three events to the endpoint and blocks anything the `security/email-regex` guardrail flags. Replace the selector with your own.

```json theme={"dark"}
{
  "env": {
    "TFY_GUARDRAIL_URL": "https://<your-gateway-host>",
    "TFY_API_KEY": "your-truefoundry-api-key"
  },
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST \"$TFY_GUARDRAIL_URL/hooks/claude-code\" -H \"x-tfy-api-key: $TFY_API_KEY\" -H 'content-type: application/json' -H 'x-tfy-guardrails: {\"llm_input_guardrails\":[\"security/email-regex\"]}' -d @-"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST \"$TFY_GUARDRAIL_URL/hooks/claude-code\" -H \"x-tfy-api-key: $TFY_API_KEY\" -H 'content-type: application/json' -H 'x-tfy-guardrails: {\"mcp_tool_pre_invoke_guardrails\":[\"security/email-regex\"]}' -d @-"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST \"$TFY_GUARDRAIL_URL/hooks/claude-code\" -H \"x-tfy-api-key: $TFY_API_KEY\" -H 'content-type: application/json' -H 'x-tfy-guardrails: {\"mcp_tool_post_invoke_guardrails\":[\"security/email-regex\"]}' -d @-"
          }
        ]
      }
    ]
  }
}
```

<Note>
  `TFY_GUARDRAIL_URL` is your **gateway host root** — the same host as `ANTHROPIC_BASE_URL` but **without** the `/api/llm` suffix (the hook endpoint is mounted at the root, e.g. `https://your-gateway-host/hooks/claude-code`).
</Note>

<Tip>
  Claude Code also supports native `"type": "http"` hooks. If you use those, put `x-tfy-api-key` and `x-tfy-guardrails` in the hook's `headers`, reference secrets as `$TFY_API_KEY`, and list them in `allowedEnvVars` so they are interpolated from the environment.
</Tip>

## Verify

Send a payload through the endpoint directly before wiring it into Claude Code:

```bash theme={"dark"}
export TFY_API_KEY="your-truefoundry-api-key"
export TFY_GUARDRAIL_URL="https://<your-gateway-host>"

echo '{"hook_event_name":"UserPromptSubmit","prompt":"reach me at a@b.com"}' \
| curl -s -X POST "$TFY_GUARDRAIL_URL/hooks/claude-code" \
    -H "x-tfy-api-key: $TFY_API_KEY" -H 'content-type: application/json' \
    -H 'x-tfy-guardrails: {"llm_input_guardrails":["security/email-regex"]}' -d @-
```

A blocked request returns `decision: "block"` (and, for `PreToolUse`, `hookSpecificOutput.permissionDecision: "deny"`). A passing request returns `{"verdict": true, "transformed": false}`.

## Response contract

```json theme={"dark"}
{
  "verdict": true,
  "transformed": false,
  "transformedContent": "...",
  "errorMessage": "...",
  "decision": "block",
  "hookSpecificOutput": { "...": "native Claude Code fields" }
}
```

* **`verdict: false`** — the guardrail blocked. The response carries Claude Code's native block fields for the event (`decision: "block"`, plus `permissionDecision: "deny"` for `PreToolUse`; for `PostToolUse` the flagged result is replaced with the guardrail message via `updatedToolOutput`).
* **`transformed: true`** — a mutating guardrail rewrote the content. The rewrite is applied through the native field for the event (`updatedInput` for `PreToolUse`, `updatedToolOutput` for `PostToolUse`).
* **Fail-closed** — if the gateway hits an error (an internal failure, or a misconfigured/unresolvable guardrail selector) it returns `verdict: false` with the event's native block fields, so a request it couldn't verify is blocked rather than let through.

## Caveats

* **`PostToolUse` cannot prevent a tool from executing.** It fires *after* the tool has run, so its side effects (a file written, a command executed, an API called) have already happened. Blocking at `PostToolUse` only replaces the result the model sees — it does not undo the action. **To actually stop a dangerous tool call, use `PreToolUse`.**
* **`UserPromptSubmit` is validation-only.** You can block a prompt, but you cannot redact or rewrite it — Claude Code has no mechanism to substitute a modified prompt. If a prompt violates policy, it's blocked, not sanitized.
* **No LLM-output guardrails via hooks.** Claude Code exposes no hook for the assistant's generated response, so model *output* can't be scanned here. Use output guardrails on the model request path instead ([Guardrails](/docs/ai-gateway/guardrails-getting-started)).
* **`PreToolUse` mutation auto-allows the call.** When a guardrail rewrites the tool input, the response sets `permissionDecision: "allow"` alongside `updatedInput`, so the sanitized call proceeds **without the usual permission prompt**. This is intentional (the guardrail already vetted it), but it does bypass interactive approval for that call.
* **Selection is header-driven, not rule-based.** The endpoint runs exactly the guardrails named in the `x-tfy-guardrails` header; it does **not** consult tenant Guardrail Config rules. Each Claude Code hook config carries its own selector list.
* **Fail-closed on errors.** If the gateway errors internally — including when a nominated guardrail selector can't be resolved (unknown name, invalid placement, unsupported type) — it returns `verdict: false` and **blocks** the action, so a request that couldn't be verified never slips through. This only applies when the gateway is actually *reached*: if it's unreachable and `curl` returns nothing, Claude Code has no decision to act on and the action proceeds (see the next point) — so hooks still shouldn't be your only control for hard-security requirements.
* **Command hooks depend on `curl` output.** For `"type": "command"` hooks, Claude Code reads the guardrail decision from the command's stdout (`curl … -d @-`). A network failure, wrong URL, or missing `x-tfy-api-key` yields no decision and the action proceeds.
* **`matcher: "*"`** on the tool hooks runs the guardrail on **every** tool. Narrow it (e.g. `"Bash"`, `"Edit|Write"`, or a specific MCP tool) if you only want to guard certain tools.
