# 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): 1. **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. 2. **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. 3. **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. 4. **Citation duty** — every SAP behavioral claim recorded in PRD/ADR/KNOWLEDGE carries `[source: , 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.sh` and 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 `jq` or an unresolvable marker now denies (fail closed); `jq` is 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.sh` pipe-tests both scripts (37 cases) against an override marker so the real marker is never touched.