> For the complete documentation index, see [llms.txt](https://docs.kyvvu.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kyvvu.com/integrations/claude-code.md).

# Claude Code

**What you'll learn:** How to add Kyvvu agent security to Claude Code sessions using the `kyvvu-claude` package.

***

## Overview

`kyvvu-claude` is a standalone Python package that integrates Claude Code into the Kyvvu ecosystem via Claude Code's native hook system. Every tool call (Bash, Read, Write, Edit, etc.) is evaluated against Kyvvu policies before execution, and all tool calls are recorded as a behavioral audit trail (when the trace goes to the platform, the platform hash-chains it on ingestion, chain v1; the hook process does not).

Unlike the SDK integrations (LangChain, CrewAI), kyvvu-claude does not wrap your code — it operates via Claude Code's hook system, so it works with any Claude Code session without code changes.

## Installation

```bash
pip install kyvvu-claude
```

## Quick start

```bash
# Interactive setup — connects to API, registers agent, installs hooks
kyvvu-claude init
```

You'll need:

* A Kyvvu API key: in the dashboard, **Workspace → API Keys**
* Python 3.10+ on macOS or Linux

After init, every Claude Code session is automatically governed.

### What init asks, and the options that answer it

On a terminal, `init` asks for the API URL, the API key (read with echo off), the agent key, the environment and the agent's owner, then where logs and incidents go, monitor or enforce, and the fail mode. Each of the first five has an option, and an option given is not asked:

| Option          | Default                             | Notes                                                                                                                                                              |
| --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--api-url`     | `https://platform.kyvvu.com`        | Also read from `KV_API_URL`                                                                                                                                        |
| `--api-key`     | (asked)                             | Also read from `KV_API_KEY`, which is better: an option shows in the process list and shell history. Printed only masked                                           |
| `--agent-key`   | `claude-code-<hostname>-<user>`     | The key the agent registers under. Pass the same one when you re-run `init`, or it registers a new agent                                                           |
| `--environment` | `KV_ENVIRONMENT`, else `production` | `development`, `qa`, `staging` or `production`                                                                                                                     |
| `--owner`       | the API key owner's email           | The workspace member accountable for the agent (EU AI Act Article 16), sent as `owner_id`. It must be the email of a member of the workspace that owns the API key |
| `--fail-mode`   | `open`                              | `open` or `closed`, see the configuration reference                                                                                                                |

With stdin piped rather than a terminal, `init` reads the remaining answers one per line and does not ask for the agent key, environment, owner or fail mode, so a script sets those with their options. A scripted setup:

```bash
export KV_API_KEY=KvKey-...        # Workspace → API Keys
printf '\nkv\n\n\nn\ny\n' | kyvvu-claude init --api-url https://platform.kyvvu.com \
    --owner ada@example.com --environment development
```

```
Kyvvu Claude Code Integration Setup

Kyvvu API URL: https://platform.kyvvu.com (from --api-url / KV_API_URL)
API key: KvKey-A**...wxyz (from --api-key / KV_API_KEY)
Log location (empty=same as API, 'stdout'=local, 'none'=disabled, or URL/path) []: Log format (kv, json, otlp) [kv]: Incident location (empty=same as trace sink, 'stdout'=local, 'none'=disabled, or URL/path) []: Incident format (empty=same as log format, or kv, json, otlp) []: Enable enforcement mode? (blocks violations) [y/N]:
Success! Agent registered: agent_3f9c2a1b7d4e8f60 (agent key claude-code-laptop-ada)
Refreshed: 20 policies loaded.
Install hooks into Claude Code now? [Y/n]: Hooks installed at /home/ada/.claude/settings.json
```

The six piped lines answer, in order: log location (empty: the platform), log format, incident location, incident format, enforce (`n`), and install hooks (`y`). Exported, `KV_API_URL` and `KV_API_KEY` are used by an interactive `init` too; it says so, and prints the key only masked. A key typed or piped in is never printed at all.

### Checking the result

```
$ kyvvu-claude status
kyvvu-claude status

  Initialized:  yes
  Agent ID:     agent_3f9c2a1b7d4e8f60
  Agent key:    claude-code-laptop-ada
  Owner:        ada@example.com
  Environment:  development
  API URL:      https://platform.kyvvu.com
  Log location: https://platform.kyvvu.com
  Log format:   kv
  Incident location: https://platform.kyvvu.com
  Incident format:   kv
  API key:      KvKey-A**...wxyz
  Mode:         monitor (violations are logged, not blocked)
  Fail mode:    open
  Policies:     20 cached
  Enforcement:  evaluating 20 policies (monitor mode: violations are logged, not blocked)
  Sessions:     0 active
  Permissions:  private (0600 files, 0700 directories)
  Hooks:        installed
```

`Enforcement` says `enforcing` only when `enforce` is `true` and violations are blocked; in monitor mode the same policies are `evaluating`. `Owner` is the owner the platform recorded, which is the API key owner's email when `init` was given none.

## Architecture

```
Claude Code                          kyvvu-claude (hook process)
    |                                     |
    +-- SessionStart ──────────────> Fetch policies, start task
    +-- PreToolUse (per tool) ─────> Evaluate policies → allow / deny
    +-- PostToolUse (per tool) ────> Record step in audit trail
    +-- SubagentStart / SubagentStop > Link sub-agent steps to the step that spawned them
    +-- SessionEnd ────────────────> Flush trace to API, end task
```

Each hook invocation is a **separate short-lived process**. There is no long-running daemon. Session state (task ID, step history, policy cache) is persisted to `~/.kyvvu-claude/` between invocations.

### Policy evaluation flow

1. **SessionStart**: Fetch policies from the Kyvvu API (filtered by this agent's key). Cache to disk.
2. **PreToolUse**: Load policies from disk cache. Replay session history into the engine for path-dependent rules. Evaluate the intended tool call. Return `allow` or `deny`.
3. **PostToolUse**: Record the completed tool call in session history. Capture LLM calls from the transcript.
4. **SessionEnd**: Record `task.end`. Flush the full behavioral trace to the API.

## Enforcement

When a policy blocks a tool call:

1. kyvvu-claude returns `permissionDecision: "deny"` to Claude Code
2. Claude Code shows the block reason to the user/model
3. The blocked step is recorded in the audit trail with `output.status = "blocked"`
4. An incident is sent to the incident sink (the Kyvvu API by default) without holding up the tool call; the hook process gives it a short flush when it exits
5. The model continues reasoning (it may try alternative approaches)

Tool blocks **deny the specific tool call** but **do not stop the session**. This is consistent with how Claude Code handles permission denials from users.

### Tainted-path enforcement

When Claude Code reads a secret file (`.env`, `.pem`, `.key`, etc.), kyvvu-claude classifies it with `data.classification: secret`. Tainted-path policies then block subsequent dangerous actions:

* `No exec after secret read` — blocks all Bash after a secret file read
* `No network after secret read` — blocks every further resource read after a secret file read (web fetches and other file reads alike), so nothing read afterwards can be sent out. This is deliberately conservative

The taint is **permanent within the session**. Use `/clear` in Claude Code to start a new session and reset.

### What this does not prevent

For Bash calls, kyvvu-claude uses regular expressions over the command text to infer secret-file reads and write targets. This is a best-effort signal, not a shell security boundary. A `python -c` or here-doc program can write outside the project without a detected target. Writes hidden behind `bash -c`, `eval`, `xargs`, or base64-decoded commands can do the same. Secret reads through a variable, split quotes such as `cat .en''v`, command substitution such as `env $(cat .en''v)`, or backticks can escape classification. The reverse also happens: `echo "value > /etc/hosts"` only prints text, but the `>` inside quotes is treated as a redirect and can trigger a false block.

Use OS controls alongside the hook: a read-only bind mount over protected paths, a restricted `PATH`, network egress controls, and an agent user that cannot read secret files. We intentionally do not add a shell parser because syntactic parsing still cannot resolve `eval`, variables, or decoded commands at runtime.

## Default policies

You do not need to assign anything. When `kyvvu-claude init` registers the agent, the platform applies Kyvvu's baseline in its **Claude Code** form:

| manifest                                                                       | policies | what it does                                                                                                                                              |
| ------------------------------------------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code Safety (`manifests/developer/claude-code-safety.yaml`)             | 13       | credential exfiltration after a secret read, destructive commands (`rm -rf /`, `git push --force`, `git reset --hard`), project containment, runaway exec |
| EU AI Act minimal/limited risk (`manifests/compliance/eu-ai-act-minimal.yaml`) | 7        | registration documentation and Article 50 transparency                                                                                                    |

Every other kind of agent gets the OWASP Top 10 for Agentic Applications template in place of Claude Code Safety. A Claude Code agent does not, because two of its policies cannot work with Claude Code: "Code execution requires a preceding gate" needs an approval step recorded before every shell command, and Claude Code does not report its own permission prompts to a hook, so it would deny every Bash call; and the tool allowlist cannot name MCP tools, which exist only once a session calls them. The choice is made by the platform from the registration's `metadata.integration` (`kyvvu-claude`).

An agent registered before this change has the OWASP template applied; its next registration retires it and applies Claude Code Safety, once. If you assign the OWASP template back yourself afterwards, it stays.

The policies block rather than warn when `enforce` is `true`; in the default monitor mode they are evaluated and recorded but nothing is denied. Run `kyvvu-claude policies`, or open the agent in the dashboard (tagged **Baseline**), for the current list. Unassigning either manifest works as for any baseline manifest, see [Removing and restoring the baseline](/policy-authoring/bundled-baseline.md#removing-and-restoring-the-baseline).

### Adding more manifests

The baseline needs nothing from you. To add a manifest on top of it, use the dashboard (**Manifests**, then assign the manifest to the agent) or the `kyvvu` CLI, which is a separate package: `pip install kyvvu-claude` does not install it.

```bash
pip install kyvvu
kyvvu auth                                   # a CLI session, not the agent's API key
kyvvu-claude status                          # the "Agent key" line names the agent
kyvvu list-manifests                         # the REPO ID and FILE of what you can assign
kyvvu assign-manifest --agent-id claude-code-laptop-ada --repo-id <id> --manifest <path>
```

`--agent-id` takes the agent key from `kyvvu-claude status` or the `Agent ID` beside it. The new policies reach Claude Code at the next session start, or straight away with `kyvvu-claude refresh`. See [`kyvvu assign-manifest`](/cli-reference/inspect.md#kyvvu-assign-manifest).

## Configuration reference

All settings are in `~/.kyvvu-claude/config.json`, configured during `kyvvu-claude init`:

| Setting                  | Default                             | Description                                                                                                                                                                                                        |
| ------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `api_url`                | `https://platform.kyvvu.com`        | Kyvvu API URL                                                                                                                                                                                                      |
| `log_location`           | (empty)                             | Where logs go: URL, file path, `stdout`, or `none`. Empty follows the policy source, which is the platform (`api_url`)                                                                                             |
| `log_format`             | `kv`                                | Log format: `kv`, `json`, or `otlp`                                                                                                                                                                                |
| `incident_location`      | (inherits trace sink)               | Where incidents go. Same vocabulary as `log_location`; empty inherits the trace sink                                                                                                                               |
| `incident_format`        | (inherits log\_format)              | Incident format: `kv`, `json`, or `otlp`. Empty inherits `log_format`                                                                                                                                              |
| `enforce`                | `false`                             | `true` = block violations, `false` = observe only (warn, don't block)                                                                                                                                              |
| `agent_key`              | `claude-code-<hostname>-<user>`     | The key the agent registers under. Set with `kyvvu-claude init --agent-key`                                                                                                                                        |
| `owner_id`               | (empty)                             | Email of the workspace member accountable for the agent, sent as `owner_id` at registration. Empty lets the platform use the API key owner's email. Set with `kyvvu-claude init --owner`                           |
| `environment`            | `KV_ENVIRONMENT`, else `production` | Deployment environment the agent registers under. A value saved in the config file wins over the variable. Set with `kyvvu-claude init --environment`                                                              |
| `fail_mode`              | `open`                              | `open` = allow when a hook or evaluation fails; `closed` = deny PreToolUse when a check fails or no policies are loaded, even in monitor mode. Set with `kyvvu-claude init --fail-mode closed` or in `config.json` |
| `risk_classification`    | `limited`                           | EU AI Act tier: `high`, `limited`, or `minimal`                                                                                                                                                                    |
| `flush_threshold`        | `100`                               | Flush logs every N steps (0 = no count-based flush)                                                                                                                                                                |
| `flush_interval_seconds` | `60`                                | Flush old pending steps on the next hook (0 = no interval flush)                                                                                                                                                   |
| `log_full_content`       | `false`                             | `false` exports tool input and output as `{redacted, bytes, sha256}` summaries; `true` exports the full recorded input, output and properties. Local policy checks always use full input                           |

One setting is not in `config.json` but read from the environment Claude Code runs its hooks in:

| Variable                          | Default | Description                                                                                                                                                                                                                                                                                              |
| --------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KV_POLICY_CACHE_MAX_AGE_SECONDS` | `86400` | `policy_cache_max_age_seconds`: how long a cached policy answer counts as current. Past it, a set that could not be refreshed is reported as stale, and a cached "no policies configured" stops being honoured as an allow in enforce mode (see [Nothing is being enforced](#nothing-is-being-enforced)) |

### OTLP export

To send traces to an OpenTelemetry collector (Jaeger, Grafana Tempo, etc.):

```bash
kyvvu-claude init
# Log location: http://localhost:4318
# Log format: otlp
```

## CLI reference

| Command                        | Description                                                                                                                                                                                            |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `kyvvu-claude init`            | Interactive setup. `--api-url`, `--api-key`, `--agent-key`, `--environment`, `--owner` and `--fail-mode` answer their prompts, see [What init asks](#what-init-asks-and-the-options-that-answer-it)    |
| `kyvvu-claude install-hooks`   | Install hooks into `~/.claude/settings.json`. `--project` writes `.claude/settings.json` in the current directory instead, which governs Claude Code only in that project and can be committed with it |
| `kyvvu-claude uninstall-hooks` | Remove hooks. `--project` removes them from `.claude/settings.json` in the current directory                                                                                                           |
| `kyvvu-claude status`          | Show configuration and status                                                                                                                                                                          |
| `kyvvu-claude policies`        | Display cached policies                                                                                                                                                                                |
| `kyvvu-claude refresh`         | Force policy refresh                                                                                                                                                                                   |

## Compatibility

Works with all Claude Code surfaces:

* Claude Code CLI
* VS Code extension
* JetBrains plugin
* GitHub Actions (`anthropics/claude-code-action`)

**Not supported:** Claude Desktop (uses MCP, not hooks).

## Troubleshooting

### Nothing is being enforced

A session says how many policies are in force, and *why* when the answer is none. The states are distinct, and they need different fixes:

| Session banner / `status`                                                                                                                    | What happened                                                                                                                                                                                                                                                                                                                                                                                    | Fix                                                                                            |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| `Policies: N loaded`                                                                                                                         | N policies are being applied.                                                                                                                                                                                                                                                                                                                                                                    | —                                                                                              |
| `Policies: N loaded (cache not refreshed)`                                                                                                   | The last fetch failed; the last known set is still being applied.                                                                                                                                                                                                                                                                                                                                | `kyvvu-claude refresh` when the API is reachable again.                                        |
| **`UPGRADE REQUIRED`** / `Policies: N loaded (kyvvu-claude too old to refresh them)`                                                         | The platform refused this kyvvu-claude as too old to serve (`426 client_upgrade_required`). The N cached policies are still enforced, but they will not be refreshed. `kyvvu-claude status` says `enforcing N policies from an unrefreshed cache — UPGRADE REQUIRED`, and the hook's stderr line ends `still enforcing the N policies in the on-disk policy cache; pip install -U kyvvu-claude`. | `pip install -U kyvvu-claude`, then `kyvvu-claude refresh`.                                    |
| `No policies are configured for this agent`                                                                                                  | The platform answered, and this agent has nothing assigned.                                                                                                                                                                                                                                                                                                                                      | Assign a manifest or create a policy.                                                          |
| **`NOT ENFORCING`** — the last recorded fetch did not succeed                                                                                | A fetch failed and there was no usable cache. **No tool call is being checked.**                                                                                                                                                                                                                                                                                                                 | Check `api_url`, the API key and network reachability, then `kyvvu-claude refresh`.            |
| **`NOT ENFORCING`** — the policy set was REJECTED                                                                                            | The set arrived but a policy in it could not be loaded (unknown `rule_type`, invalid or missing params, a duplicate name, a regex that does not compile). A set loads whole or not at all, so none of it is applied.                                                                                                                                                                             | Fix the policy definitions named in the message; `kyvvu-claude status` shows the detail.       |
| **`NOT ENFORCING`** — the platform refused this kyvvu-claude as too old; `Policies: NONE LOADED (kyvvu-claude is too old for this platform)` | The platform refused this version (`426 client_upgrade_required`) and there is no cached set to fall back on. **No tool call is being checked.** The network and the API key are fine.                                                                                                                                                                                                           | `pip install -U kyvvu-claude`, then `kyvvu-claude refresh`.                                    |
| **`NOT ENFORCING`** — the policy cache could not be read                                                                                     | The cache file exists and is corrupt, unreadable or not a policy list.                                                                                                                                                                                                                                                                                                                           | Delete `~/.kyvvu-claude/policy_cache.json` and start a new session, or `kyvvu-claude refresh`. |
| **`NOT ENFORCING`** — no cache recording a successful fetch                                                                                  | Nothing has been cached yet, or the cache could not be written.                                                                                                                                                                                                                                                                                                                                  | Start a new session, or `kyvvu-claude refresh`. Check that `~/.kyvvu-claude` is writable.      |
| **`NOT ENFORCING`** — the policy state could not be determined                                                                               | Something unforeseen went wrong while loading.                                                                                                                                                                                                                                                                                                                                                   | `kyvvu-claude status`, then `kyvvu-claude refresh`.                                            |
| **`WILL STOP ENFORCING`** — fetched but not cached                                                                                           | The fetch succeeded and *this* session is enforcing it, but the result could not be written, and every hook process reads policy from that file.                                                                                                                                                                                                                                                 | Make `~/.kyvvu-claude` writable, then `kyvvu-claude refresh`.                                  |

`kyvvu-claude status` prints the same verdict, and the session log records an unchecked tool call as `UNCHECKED`, never `ALLOWED`.

In **enforce** mode, every `NOT ENFORCING` row *denies* every tool call rather than allowing it unchecked — including a corrupt cache, which is deliberate: a session that cannot read its policies is in exactly the position of one that never had any. `No policies are configured` stays an allow in both modes, so an organisation with nothing assigned yet remains usable, and it keeps that tolerance across a failed refresh for as long as the recorded answer is within `policy_cache_max_age_seconds`.

### Agent not appearing in dashboard

kyvvu-claude caches its registration in `~/.kyvvu-claude/registration_cache.json` and skips the API call while nothing in the registration has changed. Clear it and re-initialize:

```bash
rm -f ~/.kyvvu-claude/registration_cache.json
kyvvu-claude init
```

`rm -rf ~/.kyvvu-claude` also works, but deletes the configuration, the policy cache and the session state along with it.

### Hooks not firing

`kyvvu-claude status` checks `~/.claude/settings.json` and names every hook event that is missing. If any are, run `kyvvu-claude install-hooks`. Hooks installed with `--project` are in `.claude/settings.json` of that project, which `status` does not read.

### LLM calls missing from trace

LLM calls are captured from Claude Code's session transcript. In very short sessions (<10 seconds), the transcript may not be flushed to disk before the hook reads it. This is a known limitation.
