Native memory import & export
dent8 import and dent8 export --target are the native-memory write path: they move
durable facts between the dent8 fact-event store and provider-native memory/rules files
(CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules/*, …). Both halves go through the
existing firewall, never around it. This is the implementation of items 5 and 6 in
agent-adapters.md (previously design-only).
The invariant from agent-adapters.md still holds:
Native memory/rules files are projection and integration surfaces. The source of truth is still the dent8 fact-event log.
Workflow
Section titled “Workflow”- Seed the store once. Capture durable facts through the normal path —
dent8 assert,dent8 capture, ordent8 importfrom an existing (possibly stale)CLAUDE.md. Human corrections enter assource:humanat High; agent inferences assource:agentat Low. - Keep the memory file fresh. Run
dent8 export --target CLAUDE.md(orAGENTS.md, a.cursor/rulesfile, …). Export renders the currently-believed facts into a managed block and splices it into the file idempotently, preserving all human prose outside the block. - Let the guard keep export the source of truth. The
PreToolUsenative-memory guard (dent8 hook native-memory-guard) blocks direct agent edits to these files, so the only sanctioned way to change the managed block is to re-rundent8 export.dent8 initinstalls and enforces this guard by default (opt out with--no-native-memory-guard), so a fresh project is protected out of the box — you no longer have to copy in a sample hook config. See Guard interaction below.
Because import can only assert facts and the firewall’s uniqueness rule rejects a second
fresh fact for a subject+predicate that is already believed, re-importing an exported file is a
stable no-op — the believed set does not drift. To change a believed fact, supersede it (a
fenced dent8 proposal with "op":"supersede", or dent8 supersede), not a plain re-import.
Export: the managed block
Section titled “Export: the managed block”dent8 export --target <FILE> selects facts with the same belief/freshness/contested
semantics as dent8 context: only non-terminal facts, stale/not-yet-valid excluded,
contested facts flagged. Redacted values are never written to a plaintext memory file.
The block is delimited by two sentinels and spliced idempotently:
- If the file already contains the sentinels, only the block content is replaced; all text outside is preserved byte-for-byte.
- If the file exists without sentinels, the block is appended after the existing content, separated by a blank line.
- If the file does not exist, it is created containing just the block.
Sentinels that appear inside a fenced code block (a ``` or ~~~ fence — such as the
example above) are ignored when locating the managed block, so a documented example is never
mistaken for the live block and overwritten. (Fence-awareness covers fenced code blocks only, not
4-space-indented code blocks.) A file that ends inside an unclosed fence is refused rather than
appended to, so the block is never nested inside a never-closed fence. The emitted block matches
the file’s dominant line ending (CRLF or LF) to keep line endings consistent.
The export writes the file from the CLI process itself via std::fs — the channel the
Write/Edit-scoped guard does not intercept (see below).
Block format:
<!-- BEGIN dent8 managed block (generated by `dent8 export`; edits inside are overwritten) --><!-- dent8 export: 2 believed fact(s); do not edit inside this block — run `dent8 export --target <file>` to refresh -->
### repo:demo
- `dent8://repo/demo/test_command` = "cargo test" <!-- dent8 receipt fact=fact:repo:demo:test_command:0 event_hash=152f57126ca9… authority=high source=source:human -->- `dent8://repo/demo/uses_database` = "postgres" <!-- dent8 receipt fact=fact:repo:demo:uses_database:1 event_hash=ca4354c82fbb… authority=medium source=source:human -->
<!-- END dent8 managed block -->Each fact line puts the authoritative data in the dent8://<kind>/<key>/<predicate> marker
plus the rendered value; the trailing <!-- dent8 receipt … --> comment carries the believed
event’s fact id, event_hash, authority, and source for audit (native reconcile).
Rendered value encoding:
- Text value → a JSON string (escaped, self-delimiting), e.g.
"cargo test". - JSON value →
json:followed by the canonical JSON, e.g.json:{"a":1}.
A contested fact is flagged inline with **[contested — N rival value(s); run dent8
conflicts …]**, mirroring dent8 context.
The block carries no wall-clock timestamp, so exporting an unchanged store twice produces a byte-identical file (idempotent).
Import: parsing rules
Section titled “Import: parsing rules”dent8 import <FILE> extracts durable facts with deterministic, documented rules — no LLM,
no fabrication. Every recovered proposal is routed through the exact same funnel as dent8 capture (apply_proposal → op_assert/… → authority ceiling → content check → policy →
arbitration → append). An imported fact therefore cannot smuggle itself past the firewall.
- Managed block (round-trip inverse of export). Each fact line inside a dent8 managed
block is parsed back into an
assertproposal, recovering subject/predicate from thedent8://marker and the value from the rendered token. Authority/source hints from the<!-- dent8 receipt … -->comment ride along as the proposal’s declared authority/source (the firewall still makes the final call). - Inline receipt markers in prose. A line anywhere carrying a
dent8://<kind>/<key>/<predicate>marker followed by= <value>becomes anassertproposal. This lets a human hand-annotate a durable fact in ordinary prose, e.g.Durable: dent8://repo/demo/build_tool = "cargo". - Fenced
dent8code block. A fenced block tagged```dent8whose lines are JSONProposals (the same formatdent8 capturereads, soop/authority/source/ttl/… are all available) is applied verbatim through the firewall. This is how you import asupersede,retract, or an explicitly-attributed fact.
Everything else — free prose, headings, arbitrary bullets, and other (non-dent8) code
fences — is skipped. Import never invents a fact. Skipped lines are counted and reported
(with a capped sample of reasons); a dent8:// reference that lacks a = <value> is reported
as a non-fact reference rather than silently dropped.
Flags:
--dry-runparses and reports what would be proposed, writing nothing to the store.--authority <LEVEL>/--source <SOURCE>set the default authority/source for proposals that do not declare one. A file with no attribution defaults to the agent tier (source:agentat Low), exactly like unattributeddent8 capture.
The report (text and --output json) tallies imported (accepted/contested), rejected
(with the firewall’s OpError reason), invalid (malformed proposals), and skipped
(with reasons).
Guard interaction
Section titled “Guard interaction”The PreToolUse guard (dent8 hook native-memory-guard, mode
guard-native-memory-write) blocks agent Write/Edit/MultiEdit tool calls that target a
native memory/rules file, so an agent cannot hand-edit CLAUDE.md/AGENTS.md and bypass the
fact-event firewall. This blocking behaviour is unchanged — a raw agent write of arbitrary
prose to CLAUDE.md still exits 2 under DENT8_HOOK_ENFORCE. What changed is activation:
dent8 init now wires this enforced guard into the configured agent’s hook config by default
(opt out with --no-native-memory-guard), and the installed command degrades gracefully — it is
fronted by command -v dent8 >/dev/null 2>&1 || exit 0, so a clone without the dent8 binary on
PATH allows the write (exit 0) instead of bricking every edit.
dent8 export --target is the sanctioned write path by construction: it writes the file
from the dent8 CLI process, and the guard only ever sees the agent’s tool calls, not dent8’s
own I/O. There is no new bypass — normal operation does not set
DENT8_ALLOW_NATIVE_MEMORY_WRITE.
So that the generated block reads as sanctioned rather than as unmanaged drift, the managed
block is recognized as receipt-bearing by the audit path (native scan, native reconcile,
and the guard’s post-write-audit mode). Each fact line carries a dent8:// marker, which
has_receipt_marker already recognizes; the block’s BEGIN … dent8 managed block … sentinel
is also recognized, so even an empty managed block (a store with no believed facts yet) is
treated as managed. This recognition does not weaken the write-time guard, which is path-based
and fires on the agent’s tool call regardless of file content.
Round-trip stability
Section titled “Round-trip stability”import → export --target → import is stable: the second import re-proposes already-believed
facts, which the uniqueness rule rejects, leaving the believed set unchanged. A realistic
CLAUDE.md (human prose + a fenced dent8 proposal block and/or a managed block) therefore
survives repeated export/import cycles without drift. This is exercised by the CLI integration
tests in crates/dent8-cli/tests/cli_usage.rs.
Known limitations
Section titled “Known limitations”- Contested state does not round-trip. A contested fact exports only its
currently-believed primary value, with a cosmetic
**[contested — N rival value(s)]**marker. Import is assert-only, so it reconstructs just that primary value — the rival value(s) and the contradiction edge are not recreated. To reproduce a contradiction on the destination store, re-issue it explicitly (dent8 contradict, or a fenceddent8proposal with"op":"contradict"). The[contested]marker in the block is a human warning to checkdent8 conflicts, not a machine-recoverable record. - Only text values round-trip losslessly. Import asserts text values (matching
op_assert). AJson-typed value exports asjson:<canonical>and re-imports as the equivalent text; aRedactedvalue is never written to a plaintext file, so it does not round-trip at all. - Import only asserts. A managed block or inline marker recovers
assertproposals; it never supersedes or retracts. Because the uniqueness rule rejects a second fresh fact for a subject+predicate that is already believed, re-importing an exported file is a stable no-op rather than an overwrite. Use a fenceddent8proposal block ("op":"supersede"/"op":"retract") to change an existing belief through import.