This story is becoming all too familiar: A coding agent runs rm -rf, a tilde expands to the wrong path, and twenty years of files are gone. The agent apologizes in flawless prose. The files do not come back. When it happens, experienced engineers reach for a hook, not a better prompt.
This article is a hands-on tour of how this process typically works. First we build hooks by hand in Claude Code. The event model, the exact JSON contract, and two working scripts you can drop into a repo today. Then we follow the same mechanism into production, because Sonar Vortex's agentic analysis isn't a different idea bolted onto your agent. It is this idea, a deterministic check wired into a fixed point in the agent's loop, applied to the thing that actually breaks the code the agent writes.
Why a hook, and not a better prompt
The uncomfortable admission behind the whole "harness" conversation is that you cannot instruct your way to safety. You can write "never delete files without asking" in the README. You can put it in CLAUDE.md, in capitals. It still isn't a guarantee.The agent will comply most of the time, which is precisely the problem, because "most of the time" is not a safety property. A model is a probabilistic system that samples its next move from a distribution. Capability is climbing fast; reliability on the long tail is not climbing with it. An assistant that nails 90% of tasks and fails strangely on the other 10% is useful as an assistant and a liability as an unattended autonomous system.
A hook is the opposite. It is deterministic code that runs every single time, at a known moment, whether or not the model thought to ask for it. You are wrapping a system you can't fully trust in a layer you wrote yourself. That is the entire idea, and it is why four competing harnesses (Claude Code, Codex CLI, Cursor, Gemini CLI) converged on the same primitive inside a year. When the field agrees on something that fast, it's not a fad. It's the shape of the problem.
Claude Code is the canonical implementation, so we'll use it throughout.
What are Claude Code hooks and how do they work?
A hook is a command wired into the agent's loop at a fixed event. The agent is about to do something: submit your prompt to the model, call a tool, finish its turn. Before or after that moment, your command runs. It receives a JSON description of what's happening on stdin, and it answers in one of two channels: an exit code, or a JSON object on stdout. That answer can allow, deny, or block the action; it can feed information back to the model; it can quietly fix or log things after the fact.
Claude Code exposes 30 of these events. The ones you'll use most:
Event
Fires
Typical use
SessionStart
When a session starts, resumes, or clears
Inject project context the model should always have
UserPromptSubmit
When you submit a prompt, before the model sees it
Scan or enrich the prompt; block on policy
PreToolUse
Before any tool call executes
Allow / deny / ask; the primary guardrail point
PostToolUse
After a tool call succeeds
Verify results, format, feed findings back to the model
Stop
When the agent finishes responding
Gate "done": run tests, block completion if they fail
SubagentStop
When a subagent finishes
Same, for delegated work
Notification / PreCompact / SessionEnd
On notifications, before compaction, at session end
Observability, archival, cleanup
Everything below lives in three of these: PreToolUse for prevention, PostToolUse for verification, and SessionStart for context. Those three carry the Guide and Verify story.
The contract: stdin in, exit code or JSON out
Every hook is just a program. Configuration lives in .claude/settings.json (project) or ~/.claude/settings.json (user). The structure is an event name, a list of matcher groups, and the commands to run for each match:
The matcher is matched against the tool name. "Bash" matches exactly; "Edit|Write" is a regex that matches either of the two file-writing tools; "*" matches everything. $CLAUDE_PROJECT_DIR expands to the repository root so your hook paths don't depend on the working directory.
When the event fires, Claude Code pipes a JSON object to your command's stdin. A PreToolUse payload for a shell command looks like this:
For an Edit, tool_input carries file_path, old_string, and new_string; for a Write, file_path and content. The field you'll reach for constantly is .tool_input.file_path, the file the agent just touched. (Shapes evolve between versions; run claude --debug to see the exact payload your version emits before you write jq against it.)
Your hook answers on two channels:
Exit code.0 means success; for most events, Claude Code will then parse structured JSON from stdout if you printed any. 2 is a blocking error: the action is denied (or, for PostToolUse, flagged) and whatever you wrote to stderr is fed back to the model as feedback. Any other exit code is a non-blocking error: the action proceeds and stderr is surfaced as a warning.
JSON on stdout. For fine-grained control, print a JSON object. The important part is hookSpecificOutput. For PreToolUse it carries a permissionDecision of "allow", "deny", or "ask", with a permissionDecisionReason. For PostToolUse, SessionStart, and UserPromptSubmit it carries additionalContext, a string injected into the model's context.
Two channels, one principle: exit code 2 is the blunt "no," JSON stdout is the precise "here's exactly what to do and why." Guardrails tend to use the exit code. Context injection uses JSON.
Warm-up: The guardrail from every disaster thread
Here is the canonical example: a PreToolUse hook that inspects shell commands and refuses a recursive rm whose target includes a slash, a tilde, or $HOME.
.claude/hooks/guard-bash.sh:
The agent proposes rm -rf ~/. The hook fires before the command runs, matches the pattern, and returns deny. Claude Code never executes it. The model is told no, told why, and moves on. No guessing involved: the same input produces the same decision every time.
This is both genuinely useful and genuinely limited. The pattern is deliberately blunt: it will also catch deletes you meant to run, and a determined agent can phrase a destructive command to slip past it. A regex denylist catches the failure you already imagined. It does nothing about the one you didn't.
The higher-value move: Verify what the agent writes
Deleting your home folder is the dramatic failure. The common one is quieter: the AI agent writes code that looks right, passes a glance, and carries a hard-coded credential, an injection sink, a null-dereference, or a subtle break of an architectural boundary. That code doesn't announce itself. It flows into a pull request and, often, through review.
PostToolUse is the natural place to catch it, because it fires immediately after every Edit and Write, while the change is fresh and the agent is still in the loop, long before CI. The pattern: read the file that just changed, analyze it, and if the analysis finds something serious, exit 2 with the findings on stderr. On PostToolUse, exit 2 doesn't undo the edit that already ran; it feeds your stderr back to the model, which then fixes the code before doing anything else.
A first cut might shell out to whatever linter you have:
This works, and for style and simple bugs it's fine. But lean on ruff as your safety layer and the cracks show quickly. A fast, file-oriented linter like ruff checks each file mostly in isolation, so it won't follow tainted data from an HTTP handler through three files into a SQL string, and it doesn't model your architecture, so it can't tell you the agent just made the domain layer import the web layer. Cross-file tools do exist (a type checker like tsc, a linter like clippy), but they verify types and language rules, not security taint or architectural constraints. And whatever you pick, its rule set is yours to curate and keep current, and its false positives train the agent (and you) to ignore it. You've wired up a real guardrail on a foundation built to check syntax and style, not to reason about security and design.
That gap, a deterministic hook at the right moment but shallow analysis behind it, is exactly what Sonar Vortex fills.
What is Sonar Vortex and how does it work with AI coding agents?
Sonar Vortex sits inside the agent's coding loop and does two things: it guides the agent with project-specific context before it writes, and it verifies every change in real time with SonarQube's analysis engine, the same engine teams already trust in CI. The code verification half is called agentic analysis, and the reason it belongs in this article is that, on Claude Code, it is delivered as a PostToolUse hook. It's the verify-edit.sh pattern above, except the thing behind the hook is a full static-analysis engine with your project's real context, not a single-file linter.
The interesting engineering is how it gets CI-level precision at inner-loop speed. Agentic analysis works in two phases. During a normal CI analysis, SonarQube collects and stores the context a precise analysis needs (dependencies, compiled artifacts, type hierarchies, import graphs, build configuration), tagged by project key and branch. Then, when the agent edits a file, agentic analysis restores that stored context on demand and analyzes just the changed file against it. You get the accuracy of a full scan, with the same type-aware rules and quality + profiles as CI, without re-scanning the whole project on every keystroke.
Analysis runs at two depths, and the difference matters here. STANDARD analyzes each file on its own and is the default for a single file, which is what the per-edit hook above triggers: cheap enough to sit in the inner loop after every change. DEEP adds cross-file taint tracking and can surface multi file findings and trace data flows, following tainted input across files into a sink, and is the default for a change set, so you reach it through a CLI command like sonar analyze agentic --staged or an end-of-turn verification pass (the kind the CLAUDE.md directive tells the agent to run once its edits are done). Standard keeps the per-edit loop fast; deep is where the cross-file security analysis happens.
That closes the loop. The agent writes code. The PostToolUse hook runs agentic analysis with full context. Findings, with rule keys and severities, come back into the loop. The agent fixes them and the hook re-runs. Code that would have failed the quality gate gets corrected before it ever reaches a pull request, instead of bouncing back from review days later.
Hands-on: Wire it into Claude Code
There are three setup paths. The fastest is the official plugin, which installs and configures everything for you.
Path A: The SonarQube plugin (recommended). From inside Claude Code:
Restart Claude Code (or /reload-plugins), then run the guided integration skill from a session opened in your project:
That skill installs the SonarQube CLI if it's missing, authenticates you (sonar auth login opens a browser and stores a user token in your OS keychain), and runs sonar integrate claude, which writes the hooks and MCP configuration into your project's .claude/ directory. Prerequisites: a SonarQube Cloud organization, Node.js (the SessionStart hook needs it), and a container runtime (Docker, Podman, or nerdctl) to run the MCP server image.
On the next start you'll see the SessionStart hook confirm what's live:
Read that line closely, because it's the whole thesis of this article restated by the tool itself. sonar integrate claude installs a small family of hooks:
a UserPromptSubmit hook and a PreToolUse hook for secrets detection: they scan your prompts and any file the agent is about to read or write, and block operations that would expose a credential;
a PostToolUse hook for agentic analysis (SonarQube Cloud, project-level installs) that runs analysis on your changes after edits, with no further wiring; and
the SonarQube MCP server, so the agent can pull projects, issues, and rules, plus a project-scoped context-augmentation skill for the guide half.
Every deterministic guarantee Sonar Vortex adds to Claude Code rides on the exact primitive we built by hand.
Path B: The CLI directly. If you'd rather not use the plugin, install the CLI and run one command from your repo:
Same result: the agentic-analysis hook and secrets hooks are installed and bound to your project key. Under the hood the verification step runs:
Two details worth knowing for scripting. sonar analyze agenticexits with code 51 when it reports issues and 0 when clean, a documented signal you can branch on.
The exit code gives you a clean signal to branch on, with no fragile output parsing. The case that's easy to get wrong is the last one: 0 means clean, 51 means issues were found, and anything else means the CLI itself failed (auth, a server error, a bad flag), which should surface loudly instead of falling through to a silent pass:
That last branch is a real design choice: exit 1 surfaces the failure as a non-blocking warning and lets the turn continue, while exit 2 would fail closed and make the agent stop until analysis works again. (The managed integration installs its own equivalent; this is the shape of what it does, useful if you want to customize behavior.)
Path C: The MCP server directly. If you want the analysis and context tools available to the agent explicitly, including the context-augmentation tools for the Guide phase, configure the MCP server in Claude Code's .mcp.json:
Export your token in the shell first (export SONARQUBE_TOKEN=""). The canonical agentic-analysis tool the model calls is run_advanced_code_analysis (parameters: projectKey, branchName, filePath, fileScope), which requires the local filesystem mount you see above. Two Claude-Code-specific gotchas: the context-augmentation (`cag`) tools only work with a locally running MCP server plus that mount (the Cloud-hosted MCP server doesn't expose them); and because those tools fire automatically from their descriptions, they have to be loaded from the first turn. Claude Code's tool search lazy-loads MCP tools by default, so the `alwaysLoad: true` shown above forces just the Sonar server's tools to load eagerly at startup. Setting `ENABLE_TOOL_SEARCH=false` achieves the same thing but is a blunter instrument: it disables deferral for every MCP tool you have configured, not only Sonar's.
Guide → Verify → Solve, mapped to events
Sonar frames these stages as the Agent Centric Development Cycle: a continuous Guide → Verify → Solve loop. Read through the hook lens, each pillar maps to an event:
Pillar
Event
Delivery
Guide
Inject coding guidelines, architecture, dependency health, and semantic navigation before the agent writes
Context augmentation via the installed skill + CLI/MCP tools (get_guidelines, get_current_architecture, check_dependency, …)
Verify
Analyze the changed file with full CI context; return findings
Agentic analysis via the PostToolUse hook (sonar analyze agentic / run_advanced_code_analysis)
Solve
Fix the issues found, then re-verify
The agent fixes in-loop; SonarQube's Remediation Agent and AI CodeFix handle PR and backlog issues
Guide sets the agent up to get it right the first time. Verify catches what slips through anyway. Solve closes it out. The reason the loop holds together is that each stage is a deterministic step at a fixed point, not a hope pinned on the model's good behavior.
Sonar reports the payoff across its testing and research: in its own Vortex benchmarks, up to 36% lower token consumption and 92% fewer issues, resting on a measured 3.2% false-positive rate; and separately, its State of Code survey finds teams using SonarQube are 44% less likely to experience outages caused by AI-generated code. The false-positive number is the load-bearing one: an in-loop check the agent learns to trust is worth far more than a noisy one it learns to route around.
What are the security risks of Claude Code hooks?
The mechanism that lets you customize an agent's behavior lets an attacker customize it, too. Hook definitions live in files inside the repository, so a malicious repository can ship its own. This is not hypothetical. In 2025, Check Point Research showed that a SessionStart hook planted in a project's config could execute before Claude Code's trust prompt fired: an initialization-order flaw, not a race, with the trust check simply running too late in the load sequence. That turned merely opening a cloned repo into remote code execution (CVE-2025-59536, since patched; a companion flaw, CVE-2026-21852, exfiltrated API keys through the same project-load path). Your safety layer is also your largest new attack surface.
Good implementations are paranoid about this, and the patches above are why. Claude Code gates a project's configuration behind a trust prompt the first time you open an untrusted folder, and the read-only /hooks menu lets you inspect every configured hook and see which settings file it came from. But note a sharp edge: hook definitions are hot-reloaded, so a direct edit to a settings file mid-session is normally picked up automatically by the file watcher rather than frozen at startup. That is exactly why you should treat your .claude/ hooks as security-sensitive code: pin them in version control, review changes in PRs, keep your tooling patched, and don't trust an untrusted repo's agent config unexamined. If a harness offers hooks without that posture, the hooks are the vulnerability, not the fix.
Note the shape of Sonar Vortex's own secrets protection here: a PreToolUse hook that refuses to let the agent read or write a file that would leak a credential, and a UserPromptSubmit hook that scans what you send. That's the guardrail pattern from the top of this article, pointed at a real threat: deterministic prevention, not a polite instruction.
How do Claude Code hooks fit into a broader AI agent safety strategy?
Hooks are a band of determinism wrapped around a core that will never be fully deterministic. That band is genuinely valuable (it's why this primitive spread across every major harness in a year), but a band is not a box. Pair it with real isolation: run the agent in a sandbox, a container, or a git worktree, somewhere the blast radius is a copy and not your actual home directory. Let the PreToolUse hook catch the rm -rf you thought of, let agentic analysis catch the vulnerability you didn't, and let the sandbox catch the failure nobody modeled.
The through-line of the whole "harness" conversation is that the model was never the product. The loop around the model is the product: the permissions, the sandbox, the hooks, the review gates, the analysis engine wired into the inner loop. All the boring machinery that decides what a guess is allowed to become. As models get more capable and more autonomous, that machinery isn't overhead. It's the job.
