Skip to content

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.

  1. Seed the store once. Capture durable facts through the normal path — dent8 assert, dent8 capture, or dent8 import from an existing (possibly stale) CLAUDE.md. Human corrections enter as source:human at High; agent inferences as source:agent at Low.
  2. Keep the memory file fresh. Run dent8 export --target CLAUDE.md (or AGENTS.md, a .cursor/rules file, …). Export renders the currently-believed facts into a managed block and splices it into the file idempotently, preserving all human prose outside the block.
  3. Let the guard keep export the source of truth. The PreToolUse native-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-run dent8 export. dent8 init installs 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.

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).

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_proposalop_assert/… → authority ceiling → content check → policy → arbitration → append). An imported fact therefore cannot smuggle itself past the firewall.

  1. Managed block (round-trip inverse of export). Each fact line inside a dent8 managed block is parsed back into an assert proposal, recovering subject/predicate from the dent8:// 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).
  2. Inline receipt markers in prose. A line anywhere carrying a dent8://<kind>/<key>/<predicate> marker followed by = <value> becomes an assert proposal. This lets a human hand-annotate a durable fact in ordinary prose, e.g. Durable: dent8://repo/demo/build_tool = "cargo".
  3. Fenced dent8 code block. A fenced block tagged ```dent8 whose lines are JSON Proposals (the same format dent8 capture reads, so op/authority/source/ttl/… are all available) is applied verbatim through the firewall. This is how you import a supersede, 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-run parses 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:agent at Low), exactly like unattributed dent8 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).

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.

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.

  • 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 fenced dent8 proposal with "op":"contradict"). The [contested] marker in the block is a human warning to check dent8 conflicts, not a machine-recoverable record.
  • Only text values round-trip losslessly. Import asserts text values (matching op_assert). A Json-typed value exports as json:<canonical> and re-imports as the equivalent text; a Redacted value 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 assert proposals; 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 fenced dent8 proposal block ("op":"supersede" / "op":"retract") to change an existing belief through import.