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.json falls 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

fieldmeaning
idmachine id — must equal the script filename (how the hook finds its record)
namehuman label
descriptionone-line role
categorypush (injects context, can't block) or gate (can deny a tool call)
eventharness event — UserPromptSubmit, PreToolUse, PostToolUse, Stop, …
match.toolswhich tool calls arm the hook (gates only; empty/omitted = the event itself)
script.path / script.runtimefile in hooksDir + runtime (node)
scope.projects / scope.pathswhere it applies — project slugs (["*"] = all) + path globs
enabledfalse → the hook no-ops
failPolicyon error: open (allow — push hooks) or closed (deny — security gates)
settingsthat hook's own tunables (below)

shared holds cross-hook values: paths (memoryDb, python, hooksDir), runtime.pythonTimeoutMs, projects (slugs + aliases), stopwords.

inject-protocol settings

outputcapChars (max chars injected). termsminLen, max, contextPrompts (0 = single-prompt, 2 = blend).

memory (recall)

keycontrols
maxmemories shown
minTermsskip recall below this many terms
candidatePoolbm25 rows before re-rank
gapFloordrop hits below this fraction of the top
blend.relevance/recency/confidenceranking weights
recencyHalfLifeDaysrecency decay · snippetChars per-entry truncation

skills (suggestion)

keycontrols
maxskills recommended
candidatePoolbm25 rows considered
minRelevancefloor — below it, recommend nothing
bandkeep entries ≥ this fraction of the top
minOverlaprequire ≥N distinct query-term hits (kills single-generic-word filler)
weights.name/descriptionbm25 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)

On this page