Agentkeep is a self-hosted, MIT markdown vault that your agent keeps tidy. The vault is just markdown + JSON in a folder. Open it in a compatible editor, edit it by hand, or let an agent drive it through MCP. Nothing to export, no lock-in.
MCP is the agent write path:
Run agentkeep-mcp <vault-path> and point any MCP client at it. Every agent write
goes through the write-core: content-hash compare-and-swap, atomic write, git
commit, and agentkeep-agent attribution. This provides governed undo,
attribution, and conflict protection.
Raw folder access is supported for read-only interoperability with file-only
tools. External or manual file edits remain a compatibility reality, and
Agentkeep reindexes them while it runs, but those edits bypass write-time hash
guards, automatic Agentkeep git attribution, and MCP { error, code } values.
They are not an agent connection method and are not recommended for writes.
Through MCP or the web app, every mutation is a git commit. Agent edits to an existing note can be undone in-app from that note's history; a deletion stays recoverable in git history because the removal is a commit, not an erase. External or manual file edits are ordinary filesystem edits until you or another tool commit them.
vault/
├── inbox/ # raw captures, one markdown file per item
├── notes/ # prose notes: markdown + frontmatter; [[wikilinks]]
├── memory/ # the agent's memory, one note per topic (the `remember` tool)
├── tasks/ # optional sharded JSON, one file per task
├── brief/2026-06-08.md # optional generated brief note
├── north-star.md # optional goals file used by brief helpers
└── .agentkeep/ # app state + rebuildable index cache (NOT your prose)
└── .git/ # git history at the vault root
An existing markdown vault keeps its own structure. These folders are the
fresh-vault convention, not a requirement. agentkeep init currently creates
only inbox/ and memory/; the other folders/files above are optional core
conventions and are created on demand by tools or by your agent. Agentkeep
indexes every .md file wherever it sits (dotfolders like .agentkeep/,
.obsidian/, .git/ are skipped).
A note is markdown with an optional YAML frontmatter block:
---
title: Widgets research
tags: [research, widgets]
created: "2026-06-08T09:00:00.000Z"
agent_edited: true
source: https://example.com/widgets
---
# Widgets research
Connects to [[beta]] and the #widgets project.Conventions:
[[wikilinks]]support aliases ([[target|alias]]), heading ([[target#H]]) and block ([[target^id]]) anchors and the!embed prefix are all understood; only the bare target feeds the backlink graph. The resolver is basename-based. In the web editor a[[target#H]]/[[target^id]]link navigates. Clicking it opens the target note and scrolls to that#heading/^block.![[image.ext]]renders as an inline image (served from the vault via/api/image);![[note]]renders as a read-only embed block (a card linking to the note, not a full inline copy). Full inline transclusion of a note's body is roadmap, not v1.- Tags use frontmatter
tags(a YAML list, or a comma string) and inline#tag(nested#proj/suballowed). People are a tag convention:person/<name>. titleuses frontmattertitle, else the first# H1, else the filename.source/ citation records where an agent learned something. For a URL or note, record it insource:frontmatter or an inline citation. Claims cite their source. Optional brief helpers can surface uncited agent prose for your eyes.agent_edited: trueis set by the agent when it touched a note, so the UI can visually mark agent edits. (Git authorshipagentkeep-agentis the source of truth; this flag is a convenience for fast rendering.)
- Read frontmatter by splitting only (we use
gray-matter); write a single key with a format-preserving editor (we useeemeli/yaml) so comments, key order, and whitespace survive. Never rewrite the whole block with a dumb stringify. It reorders keys and drops comments. Golden rule: an untouched file must round-trip to zero diff. - The web editor edits raw markdown source, so your exact bytes round-trip (CRLF stays CRLF, quoting is preserved).
- Quote ambiguous scalars (ISO timestamps,
yes/no/on/off) as strings, or the YAML-1.1 read side coerces them to aDate/boolean.
Structured fields markdown can't carry → one JSON file per task:
{
"id": "t_3f9a2b10",
"title": "Email Sam about the invoice",
"status": "today",
"due": "2026-06-10",
"priority": "high",
"tags": ["billing"],
"created": "2026-06-08T09:00:00.000Z",
"source": "inbox/cap_8a1c0e44.md",
"closedAt": "2026-06-09T17:30:00.000Z"
}id(required) ·title(required) ·status(required): one ofinbox|today|doing|done·created(required, ISO).- Optional:
due(ISO date) ·priority(low|med|high) ·tags(string[]) ·source(where it came from) ·closedAt(ISO, set when moved todone; the brief uses it to count "loops closed overnight"). - Pretty-printed JSON + a trailing newline so git diffs are reviewable. A malformed shard is skipped, never fatal.
Drop a timestamped markdown file in inbox/ and let the agent file it later:
---
id: cap_8a1c0e44
created: "2026-06-08T09:00:00.000Z"
type: capture
---
Remember to ship phase 5.- Filename and
idarecap_<8 hex of sha256(text)>(content-derived, so the same text maps to the same inbox file, so a double-capture is idempotent, not a dupe). - Captures are a human action; the agent reads
inbox/, then proposes notes/ tasks and (on approval) files them. - Via MCP: the
capturetool does exactly this for you.
The agent's durable memory, one plain markdown note per topic, is what the web
app's Memory page ("What your agent believes") renders. Written by the
remember MCP tool. You can open and edit a memory note like any other, but the
agent owns the file (see the clobber note below), so durable human prose belongs
in a normal note that the memory note [[wikilinks]]:
---
title: Coffee preference
type: preference
source: session 2026-06-10
updated: "2026-06-10"
---
The user takes their coffee black, no sugar. See [[Morning routine]].- Path:
memory/<slug>.md. The slug is the lowercased topic, with runs of non-alphanumerics collapsed to single dashes ('My Topic!!'becomesmy-topic). - Frontmatter:
titlekeeps the human topic (the UI shows titles, never slugs);typeis one offact|preference|person|project(defaultfact);sourceis free text recording where the agent learned it (omitted when unknown);updatedis the date of the lastremember(quoted, so it stays a string). - The
remembertool owns the whole file: each call replaces the frontmatter AND body for that topic. Re-remembering supersedes cleanly. An human-authored update requires the hash the agent previously read, so a later human edit returns a conflict instead of being overwritten. Treat a memory note as the agent's scratch copy of one belief. To keep durable human prose, put it in a normal note and[[wikilink]]it from the memory note; correcting a belief for good means telling the agent, so its nextrememberwrites the corrected version. - Body: the memory in plain markdown, wikilinked liberally so it joins the backlink graph (memory nodes are accented in the graph view).
- Honest scope: Agentkeep stores and shows memory. It does no embedding, ranking, or recall. Your agent's own context and retrieval do that.
One pinned goals file. Goals are the top-level list items (- / * ) and
## headings; plain prose paragraphs are not goals. The Morning Brief weights
"what matters" against these. The current memory-first app does not require this
file; it is a core helper convention for agents that choose to generate briefs.
- Never clobber human prose.
write_noteanddelete_notealways require the caller's observedbaseHashfor existing content.rememberrequires the currentbaseHashwhen the target memory note was last edited by a human. A conflicting human edit wins until the agent reads the new version and decides what to do. - Prefer MCP
write_notewith abaseHash. Read first, write with the hash you read. A stale hash returns a 409 conflict (your write is rejected, the human's bytes are safe). That is the feature, not a failure. - Cite your sources. Record
sourcefor anything learned externally. - Mark agent edits (
agent_edited: true) and let git carry the real provenance. - Governed writes are reversible. Each MCP/web change is one git commit
attributed to
agentkeep-humanoragentkeep-agent. Web Undo uses the governed write core. External or manual edits need their own commit discipline.
agentkeep-mcp <vault-path> serves these nine tools over stdio. write_note,
capture, remember, and delete_note mutate through the governed seam (CAS /
git rm + git commit + agentkeep-agent); the rest read.
search{ query }→ ranked{ path, title, score }[], best first.read_note{ path }→{ content, hash }. Usehashas thebaseHashfor a safewrite_note. Missing file → a not-found result (not an error throw).write_note{ path, content, baseHash? }→{ hash, commit }. OmitbaseHashto create; pass it to update. Stale hash → a 409 conflict result.list_notes{}→ every markdown path, sorted.list_tasks{}→ every task object.get_backlinks{ path }→ notes that link to the target.capture{ text }→{ path, id }; lands aninbox/file.remember{ topic, content, type?, source?, baseHash? }→{ path, hash, commit }. Upsertsmemory/<slugified-topic>.mdas the agent (frontmattertitle/type(defaultfact) /source/updated: today+ the content body. The tool owns the WHOLE file, see the Memory section above). CAS-guarded against a concurrent edit. Replacing a human-authored memory requires its currentbaseHash; missing or stale hashes return 409. Empty topic/content returns 400 (values, not throws).delete_note{ path, baseHash }→{ ok, commit }. Removes a note as the agent (git rm + commit). Use it to clear aninbox/capture once you've filed it, so the inbox empties. The removal is one commit, not a destructive erase: the note stays recoverable from git history. Read the note first and pass its hash; a stale or omitted hash returns 409. Missing path returns a 404 result; a path escaping the vault returns 400 (values, not throws).
A failing tool result comes back as an MCP tool error (isError: true) carrying
{ error, code }, for example { "code": 409, "error": "Conflict on ..." }, so the
client can react without the call throwing.