# ADR 0045: Write gates run in the store, in both tiers Status: accepted (2026-10-19). Plan: [Same gates in both tiers](/plans/memory-knowledge-layering/gates.md), step 5 of [Memory and knowledge layering](/plans/memory-knowledge-layering.md). Extends [ADR 0040](/adr/0040-memory-mechanics-in-the-server.md) (mechanics in the server, the hook as the pre-write mirror) to the content and policy gates. Part of the [decisions hub](/index/decisions.md). ## Context - The knowledge server judges every write inside the commit through `server/internal/writepolicy`: secrets refused, personal data per operator config, the world's policy document, malformed policy and template candidates. The soul server handed its store to the runtime unwrapped: no content check, no policy, on any client. The doctor found three container DSNs in this soul that the knowledge system would have refused. - A server-side warn had no channel to the writer: the knowledge server logged it and the response said `created`. Warn-level policy misses vanished the same way. - The plugin hook scans pre-write for coding agents, but a hook is one harness; CLI, TUI, library and MCP writes from other hosts reach the store directly. ## Decision - Both tiers wrap their store with `writepolicy.Enforce`. The knowledge server keeps its floor, required policy and operator content config; the soul server runs `Require: false` (a published `policy.md` governs, absent is the empty policy at warn), secrets at warn for one release then block, personal data at warn with the existing override key, no work domains, no seeded policy. - What a gate lets through with a note reaches the writer as the reserved response key `warning` on a landed write: rule and line, never the value; policy misses under warn strictness too. The key is server owned, response only, never stored. Clients render it and keep it out of derived state such as the graph observation. - The hook mirrors the scan pre-write with the same `contentcheck` options the soul server runs: a secret blocks with the fix, personal data warns. No strictness knob for secrets, no new tool, parameter, env var or config key. `marktools` scans nothing; the server is the one home. - A new server-side refusal warns for one release before it refuses, with a release note. ## Consequences - One implementation, two tiers, every client: the gap the hook could not close is closed in the store, and the hook stays the fast path with the fix in front of the agent. - The knowledge server's personal-data warns and warn-level policy misses become visible to the writer for the first time. - An untagged soul write now hears `missing-tags` from the server, as it did from the hook. A soul that wants block strictness publishes a `policy.md`. - During the warn release a secret can land on a soul from a client without the hook; the stored remedy applies (rotate, then remove). - Hook verdicts for a secret are `block`, not `ask`: an `ask` has no answerer in a headless run and only a deny is documented to return its reason to the model. ## Alternatives considered - **Scan in `marktools` only.** Rejected: covers the two MCP surfaces, not the CLI, TUI, library or an older client; a second home for a rule the server already owns. - **Hook only, as today for style.** Rejected: one harness; the three findings came through the MCP tools of this harness with the hook installed and no content rule in it. - **Server refuses from the first release.** Rejected: the delivery rule warns for one release so false positives surface before a refusal, and the warning channel had to exist first. - **A strictness knob for secrets.** Rejected: a secret is never a preference; the placeholder rule is the way to write an example password. - **Seed a `policy.md` on every soul.** Rejected: a write to people's stores for a document the empty policy already stands in for.