Claude Code¶
What you'll learn: How to add Kyvvu agent security to Claude Code sessions with the kyvvu claude commands of the kyvvu command line.
Overview¶
Claude Code support is the claude-code extra of kyvvu-cli, the kyvvu command line: it installs the kyvvu-claude plugin, which adds the kyvvu claude commands and 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 (the hook computes chain v1 through the engine; the platform verifies the chain on ingestion).
Unlike the integrations built on the Python client (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¶
uv tool install "kyvvu-cli[claude-code]"
Quick start¶
# Interactive setup — connects to API, registers agent, installs hooks
kyvvu claude setup
You'll need: - A Kyvvu API key: in the dashboard, Workspace → API Keys - Python 3.11+ on macOS or Linux
After setup, every Claude Code session is automatically governed.
Setup uses the API URL saved by kyvvu auth in ~/.kyvvu/config.toml. The
key is an agent API key: it comes from --api-key, else KV_API_KEY, else a
prompt. The kyvvu auth login is not an agent key and is never used for one.
What setup asks, and the options that answer it¶
On a terminal, setup asks for the API URL and, if needed, 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 |
saved CLI login, else 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 setup, 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, setup 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:
export KV_API_KEY=KvKey-... # Workspace → API Keys
printf '\nkv\n\n\nn\ny\n' | kyvvu claude setup --api-url https://platform.kyvvu.com \
--owner ada@example.com --environment development
Example output for user ada on host laptop; your home path and key mask
will differ:
Kyvvu Claude Code Integration Setup
Kyvvu API URL: https://platform.kyvvu.com (from --api-url / KV_API_URL)
API key: *** (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
setup 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: ***
Mode: monitor (violations are logged, not blocked)
Fail mode: open
Policies: 20 cached
Policies for: agent_3f9c2a1b7d4e8f60
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. Policies
for is the agent the platform served the cached set for; it must be the
Agent ID above, and status flags a MISMATCH when it is not (see
Troubleshooting). Owner is
the owner the platform recorded, which is the API key owner's email when
setup 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/integrations/claude-code/ between invocations.
Policy evaluation flow¶
- SessionStart: Fetch policies from the Kyvvu API (filtered by this agent's key). Cache to disk.
- PreToolUse: Load policies from disk cache. Replay session history into the engine for path-dependent rules. Evaluate the intended tool call. Return
allowordeny. - PostToolUse: Record the completed tool call in session history. Capture LLM calls from the transcript.
- SessionEnd: Record
task.end. Flush the full behavioral trace to the API.
Enforcement¶
When a policy blocks a tool call:
- kyvvu-claude returns
permissionDecision: "deny"to Claude Code - Claude Code shows the block reason to the user/model
- The blocked step is recorded in the audit trail with
output.status = "blocked" - 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
- 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 readNo 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 setup 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 with a platform release before 0.22 has the OWASP template applied; its next registration replaces it with 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.
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 command
line you installed kyvvu claude with.
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 1 --manifest manifests/operational/demo.yaml
Replace the example repository id and manifest path with values from
list-manifests. --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.
Configuration reference¶
Releases before 1.0 kept this directory at ~/.kyvvu-claude. The first kyvvu claude
command after upgrading moves everything in it here, and points hooks in
~/.claude/settings.json and the current project's .claude/settings.json that
still call the retired standalone kyvvu-claude command at the installed hook script.
All settings are in ~/.kyvvu/integrations/claude-code/config.json, configured during kyvvu claude setup:
| 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 setup --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 setup --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 setup --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 setup --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) |
OTLP export¶
To send traces to an OpenTelemetry collector (Jaeger, Grafana Tempo, etc.):
kyvvu claude setup
# Log location: http://localhost:4318
# Log format: otlp
CLI reference¶
| Command | Description |
|---|---|
kyvvu claude setup |
Interactive setup. --api-url, --api-key, --agent-key, --environment, --owner and --fail-mode answer their prompts, see What setup asks |
kyvvu claude hooks install |
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 hooks remove |
Remove hooks. --project removes them from .claude/settings.json in the current directory |
kyvvu claude status |
Show configuration and status |
kyvvu claude set |
Change a saved option, for example enforce true or fail-mode closed |
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, followed by an upgrade command. |
uv tool upgrade kyvvu-cli, 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. |
uv tool upgrade kyvvu-cli, then kyvvu claude refresh. |
NOT ENFORCING — the cached policy set was served for ANOTHER agent; Policies: NONE APPLIED (the policy set is for another agent) |
The cache holds the set the platform served for one agent and the config names another, so none of that agent's policies would apply. No tool call is being checked. See below. | kyvvu claude setup; kyvvu claude status shows both ids. |
NOT ENFORCING — the policy cache could not be read |
The cache file exists and is corrupt, unreadable or not a policy list. | Delete ~/.kyvvu/integrations/claude-code/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/integrations/claude-code 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/integrations/claude-code 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.
The policy set is for another agent¶
Every hook evaluates under the agent_id in ~/.kyvvu/integrations/claude-code/config.json, and
the platform's agent-specific policies, the claude-code baseline profile
among them, apply only to the agent they were served for. The policy cache
records that agent, and every hook compares it with the configured one. They
disagree when:
- the agent was deleted and registered again on the platform, or moved to another workspace, so it has a new id that the config does not have;
kyvvu claude setupregistered another agent and its policy refresh failed, leaving the previous agent's cache behind (a refresh that succeeds rewrites the cache);- the config was edited by hand, or copied from another machine.
The session banner, the hook's deny reason and kyvvu claude status name both
ids:
Policies for: agent_3f9c2a1b7d4e8f60 MISMATCH — this config names agent_id agent_fedcba9876543210.
Run 'kyvvu claude setup' to register this machine again and fetch its policies.
Enforcement: NOT ENFORCING (policies_unavailable) — NOT ENFORCING: the cached policy set was
served for ANOTHER agent than the one this kyvvu-claude config names, so none of that agent's
policies would apply and no tool call is being checked. Run 'kyvvu claude setup' to register this
machine again and fetch its policies; 'kyvvu claude status' shows both agent ids. Detail: the policy
set was served for agent_id 'agent_3f9c2a1b7d4e8f60' (agent_key 'claude-code-laptop-ada'); this
kyvvu-claude config names agent_id 'agent_fedcba9876543210'.
In enforce mode every tool call is denied until it is fixed, as for any
other NOT ENFORCING state. In monitor mode tool calls run and are logged
UNCHECKED with cause=agent_mismatch; with fail_mode: closed they are
denied in monitor mode too. To recover, run kyvvu claude setup: it registers
this machine's agent again, saves the id the platform returns, and fetches that
agent's policies. A config that names no agent_id at all is treated the same
way: nothing is evaluated under an empty id, and under enforce a tool call is
denied with run 'kyvvu claude setup'.
A cache written before this check recorded no agent; it is read from the one agent its policies name, and a cache of workspace-wide policies only (none names an agent) applies to any agent, as before.
Agent not appearing in dashboard¶
kyvvu-claude caches its registration in ~/.kyvvu/registrations/<agent-key>.json
and skips the API call while nothing in the registration has changed. Clear it
and run setup again:
rm -f ~/.kyvvu/registrations/claude-code-laptop-ada.json
kyvvu claude setup
rm -rf ~/.kyvvu/integrations/claude-code 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 hooks install. 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.