@promptvet/hook
v0.1.3
Published
Diary-mode hook: scans prompts with promptvet-core in the background and records results to local history. Not a gate.
Maintainers
Readme
PromptVet Hook
Every prompt scored in the background. Nothing blocked, nothing slowed down.
A Claude Code hook (PreToolUse and UserPromptSubmit) that gives you a local, queryable prompt history and audit trail — observability for your AI agent, backed by SQLite, without touching enforcement.
Wires promptvet-core's scan into Claude Code's hook system, so every prompt you (or Claude) submit gets scored automatically — and you can look back at the history any time.
What It Is
@promptvet/hook is a diary, not a gate. It listens on Claude Code's hook events, scans each prompt in the background with promptvet-core, and records the result to a local history database. It never blocks, never denies, never adds latency to what you were doing — check always exits 0, no matter what it finds.
If you want prompt quality enforcement (deny on critical issues before a tool fires), that's a separate, not-yet-built mode — see the Roadmap section of promptvet-core's README. This package is the observability half: know what you've been sending, after the fact, without lifting a finger.
Quick Start
npm install --save-dev @promptvet/hookRegister it in .claude/settings.local.json for both hook events it supports:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /path/to/node_modules/@promptvet/hook/dist/cli/index.js check --adapter claude-code --store-raw-prompt"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node /path/to/node_modules/@promptvet/hook/dist/cli/index.js check --adapter claude-code --store-raw-prompt"
}
]
}
]
}
}That's it — every prompt you type, and every prompt Claude hands to a subagent, now gets scanned and recorded automatically.
promptvet-hook reportHow It Works
┌──────────────────────────────────────────────────────────────┐
│ UserPromptSubmit → your own typed prompt, before Claude │
│ PreToolUse → Claude's own Task/Agent subagent calls │
└───────────────────────────┬────────────────────────────────────┘
│ stdin: hook event JSON
▼
┌──────────────────────────────────────────────────────────────┐
│ check --adapter claude-code │
│ Extracts the real prompt text, spawns a detached background │
│ worker, and exits 0 immediately — the calling hook never │
│ waits on a scan. │
└───────────────────────────┬────────────────────────────────────┘
│ (background, detached)
▼
┌──────────────────────────────────────────────────────────────┐
│ worker.js → scans with promptvet-core, writes one row to │
│ ~/.promptvet/history.db (SQLite, created on first use) │
└──────────────────────────────────────────────────────────────┘Two event types, one adapter. UserPromptSubmit fires on your own typed prompts — its payload carries prompt at the top level. PreToolUse fires on Claude's own outgoing tool calls — it only carries a genuine natural-language prompt when Claude is dispatching a Task/Agent subagent (tool_input.prompt); every other tool call (Bash, Read, Write, …) is correctly ignored, since a shell command or file path isn't a prompt to critique. Both shapes are handled by the same claude-code adapter, branching on hook_event_name.
Not using Claude Code? A generic --field-path adapter pulls the prompt from any dotted path in arbitrary JSON — useful for wiring in another tool's hook system once you've confirmed its payload shape. (Don't guess the shape — capture a real payload first, the same way this package's own Claude Code fixtures were built.)
Commands
check — fired by the hook, not run by hand
promptvet-hook check --adapter claude-code [--store-raw-prompt]
promptvet-hook check --field-path payload.text [--store-raw-prompt]| Flag | Meaning |
|---|---|
| --adapter claude-code | Handles both PreToolUse and UserPromptSubmit Claude Code payload shapes |
| --field-path <path> | Generic dotted-path extraction for other JSON payloads |
| --store-raw-prompt | Store the actual prompt text, not just its hash (see report --verbose below) |
Without --store-raw-prompt, only a hash of the prompt is stored — the history still tracks scores/verdicts/issues over time, just without the original text.
report — the aggregate summary (default)
promptvet-hook report
promptvet-hook report --jsonPromptVet Hook Report
─────────────────────
Total checks: 42
Verdicts:
Pass: 35
Fail: 5
Error: 2
Average score: 81.4
Top issue categories:
1. HEDGE_DETECTED 12
2. MISSING_FRAMEWORK 8
3. NO_ACTIONABLE_INTENT 5report --verbose — individual rows
promptvet-hook report --verbose
promptvet-hook report --verbose --recent 20
promptvet-hook report --verbose --jsonLists each check individually — timestamp, score, verdict, issue categories, and the prompt text itself — instead of the aggregate summary:
PromptVet Hook Report (verbose)
────────────────────────────────
1. 2026-07-04T12:05:00.000Z score: 90.0 verdict: pass
issues: (none)
prompt: Write Vitest tests for the tax calculator in src/tax.ts
2. 2026-07-04T12:00:00.000Z score: 40.0 verdict: fail
issues: NO_ACTIONABLE_INTENT
prompt: fix it| Flag | Meaning |
|---|---|
| --recent N | Cap output to the N most recent checks (ignored without --verbose) |
| --json | Full, untruncated row data as a JSON array — for scripts/tooling |
Two display rules worth knowing:
- No stored prompt? If
--store-raw-promptwasn't set for a row, the prompt column shows[hash-only, no raw prompt stored]— never the hash itself, since a hash isn't human-readable and would just be noise. - Giant or multi-line prompt? The human-readable view truncates to the first line / first 200 characters, with a
[truncated, N chars total]marker — so pasting a full terminal transcript as a prompt doesn't dump the whole thing back at you on everyreportcall.--jsonis never truncated; it always returns the full text, for anything that needs to process it programmatically.
History Storage
A local SQLite database at ~/.promptvet/history.db, created automatically on first check. Nothing leaves your machine — same zero-network philosophy as promptvet-core's scan.
| Column | Notes |
|---|---|
| timestamp, source, cwd | When, via which adapter, and from where |
| prompt_hash | Always recorded |
| raw_prompt | Only when --store-raw-prompt was passed; NULL otherwise |
| score, verdict, issue_categories | From promptvet-core's scan |
Architecture
| | |
|---|---|
| 🗒️ Diary, not a gate | check always exits 0 — a bad prompt is recorded, never blocked |
| 🚫 Never blocks the calling hook | The scan + write happen in a detached background process; the hook returns instantly |
| 🔒 Local-first | SQLite on disk, promptvet-core's scan runs locally — nothing leaves your machine |
| 🧪 TDD-built | Adapter fixtures are real captured payloads, not guessed shapes |
| 📘 TypeScript strict, Node 22, ESM only | |
Contributing
Read CLAUDE.md in the repo root first — same TDD contract as promptvet-core, plus this package's own rules (the diary-safety guarantee, the system-role/role-capture rule doesn't apply here but the "verify real payloads, don't guess" discipline very much does).
MIT Licensed · Part of the PromptVet family.
