The credential gate
kovra’s own tools never hand a sensitive value to an agent. But a credential can reach
the model by paths kovra does not control: you paste a token into the prompt, or a tool
the agent runs — cat, Read, a web fetch, another MCP server — returns a file or a
page that holds one. From then on the value sits in the model’s context, where a prompt
injection can exfiltrate it.
The credential gate closes the common paths. Claude Code asks kovra before a prompt or a tool output reaches the model, and kovra answers from a local service on your machine:
- A prompt that holds what looks like a credential is blocked whole. Nothing of it reaches the model, and Claude Code tells you why.
- A tool output that holds one reaches the model redacted: the credential is
replaced by
[REDACTED:<rule>]and the rest of the output is left as it was.
Every edition of kovra gets the same gate.
Turn it on
Section titled “Turn it on”Run kovra setup in each project where you use Claude Code (it is the same command that
connects kovra over MCP):
~/my-app % kovra setupVault ready; project `my-app`.Updated .mcp.json (register the kovra MCP server).Updated CLAUDE.md (insert/update the kovra conventions block).Updated .claude/settings.json (deny Read of secrets files).Updated .claude/settings.local.json (install the credential gate's Claude Code hooks).Credential gate: Claude Code now asks kovra before a prompt or a tool output reaches the model (it reloads hooks on its own). Notes: - Hooks fail closed on a crash only from Claude Code 2.1.295 (`onFailure`). - Recommended: set CLAUDE_CODE_DISABLE_FILE_MENTIONS=1 in your user settings (~/.claude/settings.json, "env") or your shell (Claude Code 2.1.299+). Then @file mentions go through Read and the gate. Project settings ignore it, so kovra setup does not write it.kovrad: registered to run at login.Setup complete. Review CLAUDE.md and .mcp.json, then reload your agent to pick up the MCP server.It merges two things into the project’s Claude Code settings:
- Two hooks, into
.claude/settings.local.json— your own settings, which git does not track:UserPromptSubmitrunskovra hook user-prompt-submit, andPostToolUse(every tool) runskovra hook post-tool-use— both withonFailure: "block", so a hook that crashes or times out blocks instead of letting the text through. Run from kovra.app, the command names the app’s ownkovraby its full path, so it does not depend on whichkovracomes first in yourPATH. If your repository does not ignore that file already,kovra setupadds it to.git/info/exclude. Readdeny rules, into the versioned.claude/settings.json, for files that hold secrets:.env,.env.*,*.pemand*.key..env.refsstays readable: it holds coordinates, not values, and the agent needs it.
Your own settings, hooks and rules are kept; kovra’s entries are recognised by their
command and replaced in place, so running kovra setup again changes nothing; hooks an
earlier kovra wrote into .claude/settings.json are moved out of it. The deny rules can be
committed and shared with your team; the hooks are each person’s: everyone who works in
the project runs kovra setup once. Preview everything with kovra setup --dry-run.
Claude Code reloads hooks on its own: an open session picks them up without a restart.
kovra never installs the hooks by itself. You run kovra setup, so an agent never
edits its own safety configuration.
Recommended: route @file through the gate
Section titled “Recommended: route @file through the gate”An @file mention puts a file in the prompt without firing any hook. With
CLAUDE_CODE_DISABLE_FILE_MENTIONS=1 (Claude Code 2.1.299 or later), the agent reads
the file with Read instead, and the gate sees it. Set it in your user settings —
project settings ignore it:
{ "env": { "CLAUDE_CODE_DISABLE_FILE_MENTIONS": "1" }}or export it in your shell profile.
What it catches
Section titled “What it catches”The gate recognises credentials by their documented prefix — GitHub, AWS, Slack,
Stripe, OpenAI, Anthropic, Google, private-key headers and many more: over 130 rules,
among them the useful rules of gitleaks, whose
license notice ships with kovra. Text already redacted ([REDACTED:…]) is never matched
again. A credential written with JSON escapes (\r\n, \/, \u0067…) in a text field is
found too; the gate then withholds that whole field, and blocks a prompt that holds one.
It does not recognise a credential without a prefix — a password, a bare random string. Patterns cannot tell those from ordinary text without flagging far too much.
The gate runs in kovrad
Section titled “The gate runs in kovrad”The rules run in kovrad, kovra’s per-user service. On macOS, kovra setup (and the
menu-bar app) registers it to start at login, and the hook starts it on demand if it is
not running. It makes no network connection.
~ % kovra daemon statuskovrad is running pid: 4182 version: 0.12.1 (protocol 1) uptime: 3h 12m 5s connections: 0 gate: 134 rules (set 2); 1840 clean, 1 prompts blocked, 3 outputs redacted endpoint: /Users/me/.vaults/kovrad.sockWhen kovrad cannot answer
Section titled “When kovrad cannot answer”If kovrad cannot answer, the gate fails closed. The hook does not let the text through unchecked:
- a prompt is blocked, with a message that names the cause and what to do;
- a tool output is withheld: the model receives the output’s structure with every
text field replaced by
[REDACTED:kovra-unavailable], and a note with the same cause and remedy.
A kovrad that is running gets 5 s to answer, and the hook never starts a second one beside it: a very busy Mac slows kovrad down, it does not stop it. Only when nothing is running does the hook start kovrad, within 1 s.
On a macOS release, the hook also checks that the process answering at kovrad’s endpoint is the Kaeus-signed kovrad before it trusts a verdict; a process that took the endpoint first could otherwise answer “allow” to everything. If the check fails, the gate fails closed in the same way.
That is inconvenient on purpose. The message names one of these causes, and the audit entry carries the same reason:
| Reason | The message says | What to do |
|---|---|---|
not-running |
kovrad is not running and did not start | Run kovra daemon start and send the prompt again. |
not-answering |
kovrad is running but did not answer in time | The Mac may be very busy: wait a moment and send it again. If it keeps happening, restart kovrad with kovra daemon stop and kovra daemon start. |
busy |
kovrad is at its connection limit | kovrad is answering too many requests at once: wait a moment. |
untrusted-kovrad |
the process at kovrad’s endpoint is not kovra’s signed kovrad | Stop the process the message names (its pid), then run kovra daemon start. |
newer-kovrad |
kovrad runs a newer build than this kovra | See below. |
no-vault |
kovra could not locate its vault | Check that HOME (or KOVRA_VAULT_DIR, if you set it) points to your vault. |
ipc-error, bad-verdict, too-large, malformed-input, event-mismatch |
kovrad did not answer; kovrad’s answer was not usable; the input is too large to check; the hook input was not as expected; the hook was called for another event | Run kovra daemon start and send the prompt again. |
kovra restarts kovrad only into a newer build. If an older kovra (another copy on
your PATH) finds a newer kovrad running, it leaves it alone and the gate fails closed
with a message that names both versions: point the hooks at kovra.app’s kovra (run
kovra setup with it) instead.
No hooks, no values
Section titled “No hooks, no values”kovra also refuses to deliver values to a Claude Code session that lacks the hooks. Inside such a session, the operations that put a value on stdout or into a process are refused:
- CLI:
kovra run,kovra show,kovra code,kovra decrypt,kovra ui(the dashboard),kovra package,kovra ssh-addandkovra key export; - MCP:
inject_runandreveal.
Metadata keeps working — list, status, fingerprint, doctor — so the agent can
still map your wiring.
The hooks must be in two places: the project the Claude Code session runs in — kovra
reads it from the Claude Code process itself, not from the folder a command runs in — and,
when a command runs in another project, that project too. An agent can run a command in a
folder whose settings it wrote; it cannot move the session. Hooks in your user settings
(~/.claude/settings.json) cover every project. Outside Claude Code, in your own terminal, nothing changes. The
check runs before each of those operations, so hooks removed in the middle of a
session take effect at once.
The refusal names its reason:
this Claude Code session is not protected by kovra's hooks (kovra's hooks are notinstalled), so kovra does not deliver values to it. Run `kovra setup` in`/Users/me/my-app` (or add the hooks to your user settings); Claude Code picks them upwithout a restartTo fix it, run kovra setup in the folder the message names. The other reasons point to
settings that switch the hooks off, or to a session kovra cannot place:
| Reason | What it means |
|---|---|
kovra's hooks are not installed |
Neither your user, project, local nor managed settings carry both hooks. |
hooks are disabled (disableAllHooks) |
Your settings set disableAllHooks. |
hooks are disabled by managed settings |
Your organisation’s managed settings disable hooks. |
managed settings allow only managed hooks |
allowManagedHooksOnly is set; kovra’s hooks must then be in the managed settings. |
the Claude Code process of this session was not found |
kovra found no Claude Code process above the command, so it cannot tell which project the session loaded — it refuses rather than guess. |
managed by an MDM profile kovra cannot read |
Claude Code is configured by a device-management profile, which kovra does not parse — so it refuses rather than guess. |
kovra’s hooks must run synchronously: a hook entry marked "async": true does not
hold the prompt or the output while kovra checks it, so it counts as missing.
reveal output goes through the gate too
Section titled “reveal output goes through the gate too”The MCP reveal tool returns a revealable, non-prod value on purpose. If that value
has a known prefix — a dev GitHub token, say — the gate redacts it like any other tool
output. To use such a value, run the command through kovra run (or the MCP
inject_run), which delivers it to the process and not to the model.
What it does not cover
Section titled “What it does not cover”The gate reduces how often a credential reaches the model by accident. It is not a boundary against an agent that sets out to get around it. Specifically:
- Disk and telemetry. A blocked prompt is still written to the session’s transcript on disk, and Claude Code’s telemetry sees a tool’s output before the gate replaces it. Claude Code also keeps the original structured result of an MCP tool in the transcript; kovra’s own MCP server redacts its results before returning them, but another MCP server’s results land on disk as that server returned them.
- Paths that fire no hook:
@filementions (unless you turn them off as above), the!shell mode of the CLI, and commands that fail — when a command exits with an error, Claude Code sends its whole output to the model without asking the hook. - What the model writes, and the commands it composes.
- Credentials without a prefix — passwords and bare secrets.
- A credential used as a field name of a tool’s output (a list keyed by data, such as file names). Field names are the output’s structure, which Claude Code checks, so the gate leaves them as they are and records the finding in kovra’s audit log.
- Settings kovra cannot see. Hooks configured only through the Claude Code console or
with
--settingsare not on disk, so the session check counts them as missing and refuses; on Windows, where kovra does not read the Claude Code process’s folder, a session started from another folder may load different project settings than the ones kovra finds; and an agent can remove the variables kovra uses to detect a session, setCLAUDE_PROJECT_DIRorCLAUDE_PIDto point kovra at another project’s settings, or start its command under a program of its own namedclaude. - Older Claude Code. Before 2.1.295, a hook that crashes lets the text through.
- Approvals. An agent never approves:
kovra approverefuses inside a Claude Code session, and on a Mac every approval is read and approved in kovra’s review window — the Touch ID sensor inside it, or a code and the macOS dialog on a Mac without Touch ID. Where that window can’t be shown (over SSH), what needs your approval goes to your paired phone, in kovra’s approval app (kovra Mobile, coming soon); with no paired phone, it is refused. The master key and the pairing are approved only at the Mac.
Keep sensitive values where they belong: store them with kovra add, refer to them by
coordinate, and use them through kovra run.
In the audit trail
Section titled “In the audit trail”Each verdict with a finding, each fail-closed answer and each refusal is written to the audit trail — the rule ids, the tool, the session id, never the text or the value:
| Action | When |
|---|---|
gate-block |
A prompt was blocked. |
gate-redact |
A tool output was redacted. |
gate-fail-closed |
The gate could not check the text and the hook failed closed; reason= names the cause (table above). |
session-refused |
A value was refused to a session without the hooks. |
Clean verdicts are only counted, in kovra daemon status.