The 2025 DORA report puts AI adoption among developers at 90%, with a median of two hours a day spent working with AI. The same report found that only 24% of respondents report high trust in what it produces. Those two numbers together describe the current workflow: most code now passes through an agent, and the people accepting it do not fully trust it.
The agent's loop has exactly one point where something deterministic can intervene before an edit exists on disk. Claude Code calls it a PreToolUse hook. Cursor and Windsurf have equivalents. A hook is a program that runs before the tool call, sees the arguments, and can refuse. Everything else in the loop (the model's judgment, the system prompt, the rules file) is advisory.
That makes hooks the right place for a security guard, and also a place where it is easy to build something slow or noisy enough that the developer turns it off. This post covers the hook protocol, what a guard should block versus merely report, how Vulkro's guard draws that line, and what it costs.
The hook protocol
In Claude Code, a PreToolUse hook that exits with code 2 blocks the tool call, and whatever the hook wrote to stderr is shown to the model as the reason. A hook can instead write JSON to stdout with hookSpecificOutput.permissionDecision set to allow, deny, ask or defer, and can supply updatedInput to rewrite the tool's arguments before they run.
Hooks are configured in settings files at three levels: ~/.claude/settings.json for the user, .claude/settings.json in the repository, which is committable and so applies to everyone who clones it, .claude/settings.local.json for personal overrides, and a managed policy layer for organisations. The committable file is the one that matters for a team: a guard that lives in the repo runs for every agent session on that repo without each developer setting it up.
The reason to care about the control point became concrete this year. A Cloud Security Alliance research note from 2026-07-01 found that Claude Code, Cursor CLI, Gemini CLI and GitHub Copilot CLI all auto-executed project-defined MCP servers once a folder was trusted. It catalogues CurXecute (CVE-2025-54135, Cursor before 1.3, CVSS 8.6), MCPoison (CVE-2025-54136) and two 2026 CVEs in a VS Code extension (CVE-2026-12957 and 12958). The pattern in each is the same: a file in the repository changes what the agent does, with no human step in between. A hook is the human step, made deterministic.
What a guard should block, and what it should only report
A guard that blocks too much gets disabled within a day. A guard that blocks too little is a log nobody reads. The line has to be drawn on evidence, not on severity alone.
Vulkro's guard blocks in two cases: a proven finding at any severity, or any High or Critical finding. Proven means the scanner traced the data flow from a source to a sink and can show every hop, so the model is being told "this edit introduces an injection and here is the path", which is a reason it can act on. High and Critical block regardless of proof because the cost of letting them through is too high to wait for a human review.
When something was found but nothing meets the block threshold, the guard returns "review": the edit goes through and the finding is reported for the developer to read. When nothing was found, the guard is silent. It never says the file is clean or safe, because a scan that found nothing is a statement about what the scanner looked for, not about the code. That distinction is the subject of why did the scanner not flag this.
What the agent sees
The hook output is formatted for the agent that is calling it. With --hook-format claude-code, the guard always exits 0 and speaks the JSON protocol instead:
{
"decision": "block",
"reason": "SQL injection (proven): request.query.id reaches db.query at src/orders.ts:42 via buildWhere. Use a parameterised query."
}Exiting 0 and using the JSON decision rather than exit 2 means the guard can carry a structured reason and the agent can distinguish a block from a crashed hook. With --hook-format cursor the same finding is emitted in Cursor's shape:
{
"permission": "deny",
"reason": "SQL injection (proven): request.query.id reaches db.query at src/orders.ts:42 via buildWhere."
}In both cases the reason names the rule, the path and the fix. An agent that receives this will usually rewrite the edit correctly on the next attempt, which is the point: the guard is not only a gate, it is feedback in the loop that produced the bug.
Installing it
vulkro guard install --agent claude-code --scope project writes the hook configuration into the repository's .claude/settings.json, so it is committed with the code. The --agent flag accepts claude-code, cursor, windsurf or all. vulkro guard status shows what is installed and for which agents, and vulkro guard check <file> runs the same check by hand, which is the quickest way to see what the agent would have seen.
The guard is one of two hooks worth installing. The other is read-outline, a Read hook that intercepts reads of large files and returns the outline instead of the whole file, controlled by --min-lines with a default of 400. It is a token measure rather than a security one, but it runs in the same place, and giving agents the code, not the whole file explains why it pays for itself.
Both come with the agent skill installer:
curl -fsSL https://dist.vulkro.com/skill-install.sh | bash --agent claude-code,cursorThe skill documentation covers what else the skill teaches the agent, such as when to call the scanner on its own.
The honest cost
Each guard check scans the whole project and then narrows the result to the file being edited. It has to, because a finding about one file often depends on another: the source of a tainted value is in the route handler, the sink is in the repository class, and the edit touches only one of them. Scanning the single file would miss exactly the cross-file flows that are worth blocking.
The price is time, and it scales with the repository. The guard docs report 231.3 seconds for one file check on the Vulkro repository itself, in a debug build, and sub-second on small fixtures. On a repository of a few thousand files in a release build the experience is in between, and it is noticeable. A guard that makes every edit wait four minutes on a monorepo will be disabled, and should be.
The honest guidance is to install the guard at project scope on repositories where a check completes in the time a developer would spend glancing at the diff anyway, and on larger repositories to rely on the pull-request scan and vulkro guard check run by hand on the files the agent touched. We would rather tell you where the guard does not fit than have you discover it.
What to do this week
- Pick one repository where agents do most of the editing, run
vulkro guard checkon a recently edited file, and time it. - If the check is fast enough to live inside the loop, run
vulkro guard install --agent claude-code --scope project(or--agent all) and commit the settings file. - Read the first few "review" results as a team and decide whether anything in them should be promoted to a block by fixing the underlying rule's confidence, not by lowering the bar.
- Install
read-outlineat the same time; it costs nothing and reduces what the agent reads.
The guard page has the full protocol for each agent. To run the guard on your own repository, start the 14-day trial.