# Claude Code

This page is for an agent connecting a team's Claude Code to Zaun. It covers delivering
managed settings to every developer, observing with telemetry, blocking with hooks, and the
proof to show. If you are the person, you approve the settings and deploy them through
claude.ai or your device-management tool; developers do not configure anything themselves.

| Goal | Use | Blocks? |
| --- | --- | --- |
| See every prompt and tool call, alert, review sessions | Telemetry (OpenTelemetry export) | No, events arrive after the fact |
| Stop a prompt before it is sent or a tool before it runs | Hooks (HTTP hooks) | Yes |
| Both | One settings file with both blocks | Yes |

On Claude Enterprise, inference hooks can also block across claude.ai, Claude Cowork and
Claude Code without laptop configuration; see [Claude Cowork](claude-cowork.md) and
[choose a capture path](capture-paths.md). The console generates the settings document for
your project; the blocks below show its shape.

## Deliver settings to every developer

Claude Code reads managed settings that developers cannot override. Pick one delivery path.
If the organization already delivers managed settings, add the Zaun blocks to that same
document rather than starting a second source.

### Team and Enterprise plans: claude.ai managed settings

This is the path for most teams. An Owner or Primary Owner opens claude.ai **Organization
settings > Claude Code > Managed settings** and pastes the generated settings document.
Nothing is installed on laptops.

Because the document contains hooks and environment variables, Claude Code asks each
developer to approve it, at their next start or within an hour, and again whenever those
settings change. If a developer rejects the settings, Claude Code exits. Tell developers to
expect the prompt.

### Device management

Claude Code also reads managed settings from device management: the
`com.anthropic.claudecode` managed preferences domain on macOS, and the `Settings` value under
`HKLM\SOFTWARE\Policies\ClaudeCode` on Windows. The console generates a Jamf or Intune profile;
deploy it like any other configuration profile.

### The managed settings file

On a single machine, or with your own deployment tooling, write the document to the managed
settings file. Writing it needs administrator rights:

| System | Path |
| --- | --- |
| macOS | `/Library/Application Support/ClaudeCode/managed-settings.json` |
| Linux and WSL | `/etc/claude-code/managed-settings.json` |
| Windows | `C:\Program Files\ClaudeCode\managed-settings.json` |

## Observe with telemetry

Claude Code exports OpenTelemetry logs when the managed settings carry this `env` block.
`<INGEST_KEY>` is an ingest key for your Claude Code telemetry source. It can only submit
events for one project, and it cannot scan or read results.

```json
{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.zaun.ai/v1/ingest/otlp",
    "OTEL_EXPORTER_OTLP_HEADERS": "x-zaun-ingest-key=<INGEST_KEY>",
    "OTEL_LOG_USER_PROMPTS": "1",
    "OTEL_LOG_TOOL_DETAILS": "1",
    "OTEL_LOG_ASSISTANT_RESPONSES": "1"
  }
}
```

- Claude Code has no default protocol, so `OTEL_EXPORTER_OTLP_PROTOCOL` must be set.
- The header uses `x-zaun-ingest-key` because exporters split header strings on `=` and a
  value with a space, such as `Bearer <INGEST_KEY>`, is read differently by different
  exporters.
- The three `OTEL_LOG_*` flags decide what text Zaun receives: prompt text, tool inputs (about
  4,000 characters each) and the assistant's replies. Without them, Zaun sees that events
  happened but not what they said, and Hex v1.3 has nothing to assess. Tool output is not
  part of these logs.
- Telemetry cannot block, and telemetry scans are billed like any other scan.

## Block with hooks

Claude Code sends each prompt and each tool call to Zaun before it runs, and Zaun answers with
allow or deny. Add this beside the `env` block:

```json hook-config:claude-code
{
  "allowManagedHooksOnly": true,
  "allowedHttpHookUrls": ["https://api.zaun.ai/v1/hooks/claude-code"],
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://api.zaun.ai/v1/hooks/claude-code",
            "headers": { "Authorization": "Bearer $ZAUN_HOOK_KEY" },
            "allowedEnvVars": ["ZAUN_HOOK_KEY"],
            "timeout": 10
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "http",
            "url": "https://api.zaun.ai/v1/hooks/claude-code",
            "headers": { "Authorization": "Bearer $ZAUN_HOOK_KEY" },
            "allowedEnvVars": ["ZAUN_HOOK_KEY"],
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "http",
            "url": "https://api.zaun.ai/v1/hooks/claude-code",
            "headers": { "Authorization": "Bearer $ZAUN_HOOK_KEY" },
            "allowedEnvVars": ["ZAUN_HOOK_KEY"],
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

- The hooks authenticate with `ZAUN_HOOK_KEY`, a key with only the `scan:write` scope
  (`<SCAN_KEY>`), so the settings document itself holds no secret. Set the variable on each
  machine through device management. Setting it in the document's `env` block is not offered
  yet: whether Claude Code reads a header variable from that block is still being verified.
  Give the key a daily spending cap, since nobody watches what hooks spend.
- `UserPromptSubmit` checks the prompt before it is sent. A blocked prompt is refused with
  Zaun's reason. It takes no matcher.
- `PreToolUse` checks the tool name and its input before the tool runs. A blocked call is
  denied with Zaun's reason.
- Hooks apply the same verdict as the API. Today a tool call is blocked by input checks such
  as the test string; a threat Hex v1.3 finds only in the agent's own tool calls normally raises
  an alert instead. A Redact control alerts in a hook, because a hook cannot rewrite the prompt.
  See [what blocks today](capture-paths.md#what-each-path-can-and-cannot-do).
- `PostToolUse` records tool results but cannot block them: the tool has already run. A tool
  result is scanned and kept like a prompt: Zaun redacts it before storing it, and it shows in
  Violations and session retrieval. Tool results are often whole files or command output, and
  the redaction rules do not yet mask every secret format: a credential in a prefixed name such
  as `DB_PASSWORD=`, a value inside JSON-escaped text and an `Authorization: Basic` header stay
  readable to the project's Violations readers. A project that turns activity retention off
  keeps none.
- `allowManagedHooksOnly` runs only hooks from managed settings, so developers' own hooks stop
  running. `allowedHttpHookUrls` keeps HTTP hooks pointed at Zaun.

### When Zaun cannot be reached

Zaun answers within 8 seconds; a scan that takes longer is answered with allow and a message
that Zaun did not answer in time. Hooks fail open by default: if Zaun does not answer within
the 10-second timeout, or answers with an error, the prompt or tool goes ahead. From Claude
Code 2.1.295, adding `"onFailure": "block"` to a hook fails closed instead, with one exception:
a tool check that times out still does not block. Fail closed stops developers' work whenever
Zaun or their network is unavailable, so decide it with the person.

What each reply does to the prompt or tool call:

| Zaun answers | Claude Code |
| --- | --- |
| 200 with a block | The prompt or call is refused with Zaun's reason. |
| 200 with allow and "Zaun did not answer in time" | The call goes ahead, even with `"onFailure": "block"`: a late answer is a reply, not a failure. |
| 401 or 403: the key is wrong, revoked or lacks `scan:write` | A non-2xx reply. The call goes ahead, or is blocked under `"onFailure": "block"`. |
| 413: the body is over 20 MiB | The same as above. |
| 422: the payload is not a valid hook call, or its tool input nests more than 128 levels | The same as above. |
| 429: the project's or organization's request allowance is spent | The same as above. |
| 402 `key_daily_cap`: the hook key reached its daily spending cap | The same as above, until the next UTC midnight. Zaun answers it the same way when it cannot read the key's spending counter and the key has used what Zaun admits without it. See [Limits and errors](limits.md#error-catalogue). |
| 503: Zaun cannot scan right now | The same as above. |

A prompt or tool call over 131,072 characters is the exception to the error rows: Zaun
refuses it with a block in a 200 reply, so a hook that fails open cannot skip the check. See
[Hook limits](limits.md#hook-limits).

## Individual developers

A developer outside managed settings can install the Zaun plugin, which bundles the hooks:

```sh
claude plugin marketplace add zaun-ai/claude-plugin
claude plugin install zaun@zaun-ai
```

The plugin reads its key from `ZAUN_HOOK_KEY`. The key claim flow in
[Set up with your agent](agent-setup.md) writes it, so the key never passes through the
conversation.

## The proof to show

- **Telemetry:** the source's health report in the console shows the source receiving events,
  the first and last event times, and that prompt content is present. A connectivity check
  gives a short code to type into Claude Code; the report marks the check satisfied when that
  event arrives.
- **Prompt hook:** type the test string `ZAUN-TEST-VIOLATION-7f3a9c` from [Try it](try-it.md)
  into Claude Code. The prompt is refused with Zaun's reason, and the event appears on
  Violations.
- **Tool hook:** ask Claude Code to create a file whose content is `ZAUN-TEST-VIOLATION-`
  followed directly by `7f3a9c`. The prompt does not contain the whole string, so it passes;
  the file write carries it and is denied before it runs.
