5.3 KiB
ADR-003 — Ground-truth enforcement: hooks, not advice
Date: 2026-09-04 · Status: Accepted
Context
The old estate proved that advisory guidance fails: an MCP routing table, skills, and operating rules existed for months, yet the servers and skills were never consulted during development — even on the CAP packages where they obviously applied. Causes: tool-selection dilution under a saturated loadout, zero enforcement (prose loses to momentum), friction (cold starts, deferred tool loading), and model overconfidence in trained SAP knowledge. The requirement for this repo: consultation must be structural — the agent must forget what it thinks it knows, consult the MCP servers and context7 before work starts, and be able to cite sources on demand.
Decision
Four mechanisms in .claude/settings.json + .claude/hooks/ (project scope,
part of the founding kit for every future package):
- SessionStart injection — every session opens with the grounding charter: training knowledge is not a citable source; the five MCP servers + context7 are; if tools are deferred, loading them is the first action.
- Per-prompt reflex (UserPromptSubmit) — a SAP-scoped analogue of the machine-global TOOL-REFLEX hook: never classify whether a prompt "needs" a consult; the classification step is the known failure.
- Consultation gate (PreToolUse deny) — every successful
mcp__*, WebSearch, or WebFetch call stamps.claude/.last-mcp-consult; a Write/Edit on a SAP artifact (.cds,.abap,.bdef, annotations, views,manifest.json,srv/,app/,worker/) is denied unless a consultation happened within 30 minutes, with the remedy stated in the denial reason. Governance.md,docs/, and.claude/paths are exempt so documentation never jams. - Citation duty — every SAP behavioral claim recorded in PRD/ADR/KNOWLEDGE
carries
[source: <server> <doc-id/URL>, verified YYYY-MM-DD](CLAUDE.md rule 6); a PostToolUse reminder fires on governance-doc edits. Uncited = hypothesis.
On "load tools automatically at session start"
Corrected 2026-09-04 after reading the 2026-09-03 "Investigate performance
lag issue" session transcript: my original claim ("no setting exists to preload
deferred tools") was true of settings keys but wrong in practice. The
ENABLE_TOOL_SEARCH=false environment variable disables deferral entirely
(verified against the binary in that session). It must be set before process
bootstrap — the settings.json env block loads too late, per that session's
finding — so it lives in the claude() wrapper in ~/.bashrc, where it is
already active machine-wide, including for sessions started in this repo.
History: preloading was first measured at ~98k tokens/turn at the old ~950-tool
surface and rejected in favor of the TOOL-REFLEX hook; once plugin localization
cut the surface to ~40 tools, the same session adopted preloading via the
wrapper. This repo's slim 5-server loadout keeps the preload cost small.
Mechanism 1 (SessionStart load-first order) remains as the fallback for launch
paths that bypass the wrapper (e.g. IDE-spawned sessions).
Consequences
- Consultation stops being a virtue and becomes a precondition; skipping it now produces a denied edit instead of silent drift.
- The 30-minute freshness window is a tunable; if it proves too tight or too
loose in practice, adjust it in
sap-consult-gate.shand note it here. - The gate measures that a consultation happened, not that the right one did — citation duty and review cover relevance.
- Hooks activate on the first session started inside this repo (project hooks are read at session start).
Amendment 2026-09-14 — gate hardened after code review
The 2026-09-04 gate was advisory in practice: the Write|Edit matcher missed
Bash writes (sed -i, heredocs, tee, cp/mv, interpreter one-liners), MultiEdit,
NotebookEdit and filesystem-MCP edits; the mcp__.* PostToolUse matcher let
any MCP call (memory, chrome, mcp-manage) refresh the 30-minute marker; hook
commands hard-coded this checkout's absolute path; and a missing jq produced
a silent allow. I changed the mechanism, not the prose:
- PreToolUse gate matches
Write|Edit|MultiEdit|NotebookEdit|Bash| mcp__filesystem__write_file|mcp__filesystem__edit_file|mcp__filesystem__move_file; for Bash the script gates only commands that combine a write indicator with a gated path (documented heuristic, known blind spots: variable-built paths,cd-relative paths, writes by invoked scripts). - The consult marker is refreshed only by the grounding servers (sap-docs,
abap-docs, sap-released-objects, the Fiori/UI5 servers, context7, CAP) and
WebSearch/WebFetch — anchored regex in
settings.json. - Hook commands resolve via
"${CLAUDE_PROJECT_DIR}"; scripts prefer that variable over$0[source: https://code.claude.com/docs/en/hooks — "${CLAUDE_PROJECT_DIR}: the project root where the session started", verified 2026-09-14]. - Missing
jqor an unresolvable marker now denies (fail closed);jqis a hard dependency of any session in this repo. */worker/*left the gated set: the TypeScript worker is governed by context7 and the OpenAPI contract, not the SAP gate..claude/hooks/tests/run.shpipe-tests both scripts (37 cases) against an override marker so the real marker is never touched.