Claude Code Hook
Block risky MCP server installs automatically, before they happen.
The Aevrin hook is a genuine Claude Code PreToolUse hook — a registered command, not an
advisory MCP server Claude can choose to ignore. It intercepts the two ways an MCP server
actually gets installed and checks each one against Aevrin before Claude proceeds.
What it intercepts
Bashcalls matchingclaude mcp add ...— extracts the URL for HTTP/SSE transport servers, or best-effort reconstructs a single-server config for stdio transport (-- npx .../-- docker run ...).Writecalls to.mcp.jsonorclaude_desktop_config.json— parses the full written config for server URLs or stdio commands.
Everything else is ignored silently — exits 0, no output, stays out of the way of unrelated
tool calls. Partial-diff Edit calls to these files are intentionally not intercepted:
Edit's old_string/new_string are a fragment, not the full resulting file, so there's
nothing reliable to scan pre-edit.
Setup
Install the CLI if you haven't — see Getting Started.
Log in the hook (a separate credential from aevrin login, so hook and CLI usage stay
independently attributable even for the same person):
aevrin hook setupThis prints a ready-to-paste JSON snippet pointing at the hook script bundled inside your
aevrin install — the same package you already installed via pip/pipx.
Merge the printed snippet into your project's .claude/settings.json. If the file already
has a hooks key, merge rather than overwrite it — the snippet looks like this:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "python3 /path/to/hook_script.py", "timeout": 8 }]
},
{
"matcher": "Write",
"hooks": [{ "type": "command", "command": "python3 /path/to/hook_script.py", "timeout": 8 }]
}
]
}
}Try it — ask Claude Code to add an MCP server. The hook checks it before the install runs.
Decision logic
Check for a cached result first — one fast POST /hook/cache call (a Supabase lookup,
not a live scan). The request body keeps pasted configuration out of URL logs, while the
short lookup stays within the hook's timeout budget.
Clean cached scan → allow silently, with a small confirmatory note
(Aevrin: clean scan on record (score N/100)).
Cached scan has unresolved critical/high findings → block, with the score and each
finding's title, severity, OWASP category, file/line, and remediation in the denial reason —
plus three explicit options: fix it directly, run aevrin hook allow <target> to install
anyway, or aevrin findings triage <id> false_positive --reason "..." to dispute a specific finding.
Cached scan's tools couldn't run → block as incomplete, never silently allowed. Docker down, a missing binary, or no network access on the machine that scanned it means the findings list is empty because nothing ran — not because the target is clean. See Concepts → Incomplete scans.
No cached result → allow, with a visible "not yet scanned" warning. The actual scan
starts server-side in the background (the /hook/cache endpoint kicks it off via FastAPI
BackgroundTasks) — the hook script itself never runs or waits on a scan, only ever makes
one short HTTP request. The next install attempt of the same target will have a cached
result to check against.
Fails open
Any failure mode — no API key configured, network error, timeout, malformed response — fails open (allows silently). A security hook that blocks installs whenever Aevrin itself is unreachable is a hook that gets disabled by annoyed developers, which defeats the point. This is a deliberate tradeoff: the hook is a safety net for the common case, not a hard security boundary you should rely on when Aevrin is down.
Quota
Hook auto-scans are metered separately from CLI and dashboard scans — see Concepts → Quota. Running out mid-month doesn't block installs; it allows with a note that the install wasn't checked, the same way an unreachable API does.