E:\hooks\config.json is the hook registry — a shared block plus a list of hook records. One file declares every hook: what it is, where it runs, what it triggers on, where it applies, its fail policy, and its settings.
- Single source — every hook reads this file.
- Self-locating — a hook finds its own record by
id(== its filename):inject-protocol.mjs→ the record with"id": "inject-protocol". - Defaults-merged — each hook embeds defaults; a missing key or broken
config.jsonfalls back silently (never a dead hook). - No restart — read per invocation.
Shape
{
"shared": { "paths": {…}, "runtime": {…}, "projects": {…}, "stopwords": "…" },
"hooks": [
{
"id": "governance-gate",
"name": "Governance Write Gate",
"description": "Deny agent writes under governance/** without user-typed approval.",
"category": "gate",
"event": "PreToolUse",
"match": { "tools": ["Write","Edit","MultiEdit","NotebookEdit","Bash"] },
"script": { "path": "gov-gate.mjs", "runtime": "node" },
"scope": { "projects": ["blockdata","kai-chattr","kai"], "paths": ["governance/**"] },
"enabled": false,
"failPolicy": "closed",
"settings": { "approval": { "window": 5, "keyword": "APPROVE" } }
}
]
}Record fields
| field | meaning |
|---|---|
id | machine id — must equal the script filename (how the hook finds its record) |
name | human label |
description | one-line role |
category | push (injects context, can't block) or gate (can deny a tool call) |
event | harness event — UserPromptSubmit, PreToolUse, PostToolUse, Stop, … |
match.tools | which tool calls arm the hook (gates only; empty/omitted = the event itself) |
script.path / script.runtime | file in hooksDir + runtime (node) |
scope.projects / scope.paths | where it applies — project slugs (["*"] = all) + path globs |
enabled | false → the hook no-ops |
failPolicy | on error: open (allow — push hooks) or closed (deny — security gates) |
settings | that hook's own tunables (below) |
shared holds cross-hook values: paths (memoryDb, python, hooksDir), runtime.pythonTimeoutMs, projects (slugs + aliases), stopwords.
inject-protocol settings
output — capChars (max chars injected). terms — minLen, max, contextPrompts (0 = single-prompt, 2 = blend).
memory (recall)
| key | controls |
|---|---|
max | memories shown |
minTerms | skip recall below this many terms |
candidatePool | bm25 rows before re-rank |
gapFloor | drop hits below this fraction of the top |
blend.relevance/recency/confidence | ranking weights |
recencyHalfLifeDays | recency decay · snippetChars per-entry truncation |
skills (suggestion)
| key | controls |
|---|---|
max | skills recommended |
candidatePool | bm25 rows considered |
minRelevance | floor — below it, recommend nothing |
band | keep entries ≥ this fraction of the top |
minOverlap | require ≥N distinct query-term hits (kills single-generic-word filler) |
weights.name/description | bm25 column weights (description-heavy) |
Tune cheatsheet
- turn a hook off → its record's
enabled: false - stricter skill match → raise
skills.minOverlap/skills.band/skills.minRelevance - fewer memories →
memory.max· output budget →output.capChars - thin-prompt context →
terms.contextPrompts(0= single-prompt)