Set the store policy
memhtml config # the policy in force, and where each value came fromgit -C ~/memhtml add memhtml.config.json && git -C ~/memhtml commit -m "store policy"memhtml.config.json at the store root holds the rules every writer to one store has to agree on: what the write bar refuses, whether the old record-creating commands stay open inside an agent run, and how the curator files records. With no file, every key takes the library’s default: the bar refuses no type and requires no anchor, and the old record-creating commands close only inside an agent run. A store that wants a stricter bar writes the file.
Where the file lives and when it’s read
Section titled “Where the file lives and when it’s read”The file sits beside the corpus at the store root and is tracked in git, so a clone carries its rules with it. It isn’t a memory. It’s never indexed, no session can write it, and list, publish, and the sitemap leave it out.
Every process reads the file from the working tree, with no cache between calls: the CLI once per command that judges or creates a record, the head server once per request that judges one, and the MCP server once per record-creating tool call. An edit takes effect on the next call in every process, with no restart. Commit the file so every clone agrees, and use memhtml config to check that the working-tree copy is the committed one.
A broken file can’t stop reads, search, or status, because none of them judges a record.
The keys
Section titled “The keys”Every key is optional, and an absent key is its library default.
| Key | Type | Default | What it does |
|---|---|---|---|
writeBar.refusedTypes |
an array of memory types, or of { "type", "reason" } |
[] |
Types no session may write, the curator’s included. The refusal shows the writer your reason, or a default sentence for a bare type name. |
writeBar.requireAnchor |
boolean | false |
Every new record a session writes must name what it’s about: at least one memhtml-entity, or a path under projects/<slug>/. |
v1Doors |
"open", "closed-in-session", or "closed" |
"closed-in-session" |
Whether memhtml write, apply, correct, task add, and the MCP memory_write, memory_write_batch, and memory_correct tools may create records. |
curator.writerBuckets |
an array of directory-name globs | [] |
Directory names that name a writer, not a subject. The curator’s briefing never offers one as a home, and curate collapse won’t put a canonical in its members’ bucket. |
curator.placementQuota |
an integer from 0 to 200 | 60 |
How many inbox records one curate run should file into their homes. |
writeBar
Section titled “writeBar”The write bar judges every record a session writes, through session put, session exec, session commit, and every curator command. refusedTypes takes a type by name, or with the reason the writer should read:
{ "writeBar": { "refusedTypes": [{ "type": "episodic", "reason": "a run narrative belongs in the trace index, not in memory" }, "verdict"], "requireAnchor": true }}A put that breaks the bar refuses the whole call and adds nothing:
{ "apiVersion": "1", "error": "invalid memory: session put refused, nothing appended: areas/inbox/builds-are-slow-on-mondays.html: below the write bar: names no system, project, or person: add at least one memhtml-entity (<meta name=\"memhtml-entity\" content=\"system:<name>\"> in the head, an entities value in a write op, or a label op) or write it in a workspace (projects/<slug>/)", "code": "ERR_INVALID_MEMORY", "suggestions": ["memhtml manifest"]}requireAnchor also holds for a move, whose destination has to be anchored, and for an unlabel that would leave a record with no entity outside projects/<slug>/. An archive is an existing record moving, so the anchor rule doesn’t judge it. A task is anchored like any other record, by a projects/<slug>/tasks/ path or by an entity.
v1Doors
Section titled “v1Doors”memhtml write, apply, correct, and task add commit straight to the store, and the write bar never sees them. v1Doors decides when they stay open:
| Mode | MEMHTML_SESSION set |
MEMHTML_SESSION unset |
|---|---|---|
open |
open | open |
closed-in-session |
refused with ERR_SESSION_BOUND |
open |
closed |
refused with ERR_SESSION_BOUND |
refused with ERR_V1_DOOR_CLOSED |
Both refusals exit 2 before anything opens, and their suggestions carry a session put line with placeholders to fill in. The default closes the doors only inside an agent run, because a run that writes through a session should land all its records through the bar and in one commit. closed makes sessions the only way records go in.
The MCP server has no per-call session. Its memory_write, memory_write_batch, and memory_correct tools read MEMHTML_SESSION from the server’s own environment. Under closed-in-session, a server an agent runtime starts inside a run is closed, and a shared server started outside any run is open.
No mode closes reads. No mode closes the edits of records that already exist either: link, archive, task status, and reinforce on the CLI, and memory_link, memory_archive, and memory_reinforce on MCP.
curator
Section titled “curator”writerBuckets takes globs over a single directory name, matched with * for any run of characters, ? for one, and [0-9] for a class. A / is refused, because a pattern names a directory and never a path. placementQuota is capped at 200, the most operations one curate run commit can hold. Both feed the curator’s briefing, and placementQuota also fills the {{placementQuota}} placeholder in its charter.
What no setting turns off
Section titled “What no setting turns off”The policy can only add refusals to the write bar. These hold for every session write in every store, whatever the file says:
- a file that doesn’t parse as a memory, or has no single claim
- a duplicate of an active record’s article
- a put over an active record with a different article
- a reserved path:
.memhtml/,memhtml.config.json, anyindex.htmlorsitemap.xml, and outside curationareas/arcs/andresources/people/ - more than 200 operations in one
session commit
At commit, a put whose claim has the same frame key as an active record at another path gets a memhtml-contradicts link to it, and both records stay live. A record that agrees with the put, that the put supersedes, or that the same batch archives gets no link.
A strict store
Section titled “A strict store”This file refuses run narratives and review verdicts, and requires every record a session writes to name its subject. It keeps the default doors and tells the curator that two kinds of directories are writer buckets:
{ "writeBar": { "refusedTypes": [ { "type": "episodic", "reason": "a run narrative belongs in the trace index, not in memory" }, { "type": "verdict", "reason": "review output isn't a memory; keep it with the review" } ], "requireAnchor": true }, "v1Doors": "closed-in-session", "curator": { "writerBuckets": ["*-import", "*-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]"], "placementQuota": 60 }}The second bucket pattern matches a directory named with a trailing date, such as notes-2026-10-18. The date records when a writer made the directory and says nothing about the subject.
A typo stops writes
Section titled “A typo stops writes”The file is decoded strictly, and a problem is never read as “unset”. Each of these is ERR_STORE_CONFIG at exit 1: a key the schema doesn’t know (requireAnchors for requireAnchor), a value of the wrong type, an unknown memory type, a type listed twice, JSON that doesn’t parse, and a file that can’t be read. For the typo, the CLI prints:
{ "apiVersion": "1", "error": "the store's policy file /home/you/memhtml/memhtml.config.json is not usable, so nothing that needs the policy runs: Expected no excess property at [\"writeBar\"][\"requireAnchors\"]", "code": "ERR_STORE_CONFIG", "suggestions": ["memhtml config", "fix or remove /home/you/memhtml/memhtml.config.json, then retry"]}Every command that needs the policy refuses this way until the file is fixed: session put, session exec, and session commit, the curator commands, the record-creating CLI commands and MCP tools, memhtml config itself, and the head server’s commit, exec, and neighbors routes. The rule exists because a typo in a policy file must never quietly loosen the policy it was meant to state.
Read the policy in force
Section titled “Read the policy in force”memhtml config{ "apiVersion": "1", "type": "config.report", "data": { "file": { "path": "/home/you/memhtml/memhtml.config.json", "present": true, "git": "committed" }, "store": { "writeBar.refusedTypes": { "value": [{ "type": "episodic", "reason": "a run narrative belongs in the trace index, not in memory" }], "source": "file" }, "writeBar.requireAnchor": { "value": true, "source": "file" }, "v1Doors": { "value": "closed-in-session", "source": "default" }, "curator.writerBuckets": { "value": [], "source": "default" }, "curator.placementQuota": { "value": 60, "source": "default" } }, "process": { "MEMHTML_EMBED_WARM_IDLE_MS": { "value": 0, "source": "default", "readBy": "head serve" }, "MEMHTML_EXEC_POOL_SIZE": { "value": 4, "source": "default", "readBy": "head serve" }, "MEMHTML_EXEC_WORKER_HEAP_MB": { "value": 2048, "source": "default", "readBy": "head serve" } } }}file.git says whether the working-tree copy matches git: committed, modified, untracked, or absent, and unknown when git can’t answer. Each key under store carries its effective value and a source of file or default. process lists three of the variables head serve reads, each with a source of env or default, and adds a problem when a value is one the server would refuse. The command reads only the file, its git status, and the environment, so it answers on a store whose index was never built.
Tighten the policy on a live store
Section titled “Tighten the policy on a live store”The bar is read again when a session commits, so a stricter file also judges operations that were added before you changed it. An open session can then answer refused at commit time for a line its put accepted. Before you tighten the bar, find the sessions still holding operations with memhtml session prune --older-than 0s --dry-run, or let the runtime’s --drop-refused take the refused lines out. Write through a session covers both.