Keep is a personal wiki tool for people who think carefully and want their thinking to compound over time. You write Markdown documents with structured frontmatter. Keep reads that frontmatter and produces derived artefacts — a relationship graph, a document index, a staleness report — that make your knowledge base navigable and maintainable even as it grows.
The problem Keep solves is one most people feel but rarely name: you learn things, you write things, you save things — and then you lose them. Not because they are deleted, but because they are scattered, unconnected, and contextless. Keep is the answer to I know I wrote about this before, but I cannot find it or pick up the thread.
- How it works
- Installation
- Getting started
- Workspace structure
- Frontmatter reference
- Commands
- Configuration
- Keep and LLMs
- Development
A Keep workspace is a folder of Markdown files. Each file is a document. Each document has a YAML frontmatter block at the top that describes it structurally — its slug, title, status, relations to other documents, and more.
Keep reads that frontmatter and produces four derived artefacts:
| Artefact | Location | Purpose |
|---|---|---|
graph.json |
.keep/graph.json |
Machine-readable graph of all documents and relations |
state.json |
.keep/state.json |
Staleness tracking — when was each document last meaningfully changed? |
lint.json |
.keep/lint.json |
Validation report — violations, warnings, auto-injections |
index.md |
index.md |
Human and LLM-readable catalog of all documents |
These artefacts are rebuilt deterministically on each run. You never edit them directly. The documents themselves are the source of truth.
After a full run, a workspace looks like this:
my-wiki/
├── keep.yml # workspace configuration
├── index.md # generated catalog
├── .keep/
│ ├── graph.json
│ ├── state.json
│ └── lint.json
├── recipes/
│ ├── ragu.md
│ └── soffritto.md
└── people/
└── marcella-hazan.md
Keep requires Python 3.12 or later.
From PyPI (once published):
uv tool install keepFrom source (current):
uv tool install git+https://github.com/your-username/keepVerify the installation:
keep --helpTo upgrade:
uv tool upgrade keep
# or from source:
uv tool install --force git+https://github.com/your-username/keepThis walk-through takes you from an empty directory to a linted, indexed wiki in a few minutes.
mkdir my-wiki
cd my-wikiEvery workspace needs a keep.yml at its root. Create one with the following
content — this is the minimal configuration and uses all defaults:
schema_version: 1That is all you need to start. See Configuration for the full list of options.
Create recipes/soffritto.md:
---
slug: soffritto
title: Soffritto Base
kind: recipe
status: canon
date_created: "2025-11-03"
tags: [ italian, base, vegetables ]
summary: Foundational aromatic base of onion, celery, and carrot for Italian
sauces.
---
Soffritto is the backbone of Italian cooking. Equal parts onion, celery, and
carrot, cooked slowly in olive oil until completely soft and sweet. Never brown
— the goal is a melting, fragrant base.The frontmatter fields are:
slug— a unique identifier for this document (URL-safe, kebab-case)title— the human-readable namekind— the semantic type (recipe,person,note, etc.)status— lifecycle stage (draft,review,published,canon)date_created— ISO 8601 datetags— freeform listsummary— one sentence describing the document (used inindex.md)
State tracking is the foundation for staleness detection. Run it first:
keep state
# state written to .keep/state.json (1 document(s))Keep reads each document's frontmatter, hashes the fields that matter for staleness, and records the last meaningful modification timestamp. Run this after any significant editing session.
keep graph
# graph written to .keep/graph.json (1 node(s), 0 edge(s))The graph captures all documents as nodes and all relations as typed directed edges. With one document and no relations, you get one node and no edges.
Create recipes/ragu.md:
---
slug: ragu
title: Ragù alla Bolognese
kind: recipe
status: published
date_created: "2026-01-10"
tags: [ italian, pasta, meat ]
summary: Classic slow-cooked Bolognese built on a soffritto base.
relations:
- target: soffritto
type: derived_from
---
A proper Bolognese takes time. The soffritto is the foundation.The relations field declares that ragu derives from soffritto. This is a
typed directed edge in the graph.
keep graph
# graph written to .keep/graph.json (2 node(s), 1 edge(s))Two documents, one relation.
keep index
# index written to index.md (2 document(s))Open index.md:
<!-- Auto-generated by keep. Do not edit. -->
| slug | title | kind | status | summary |
| --------- | ------------------- | ------ | --------- | --------------------------------------------------------------------------- |
| ragu | Ragù alla Bolognese | recipe | published | Classic slow-cooked Bolognese built on a soffritto base. |
| soffritto | Soffritto Base | recipe | canon | Foundational aromatic base of onion, celery, and carrot for Italian sauces. |This is the catalog an LLM reads at the start of every session to locate relevant documents before drilling into them.
keep lint
# lint: ✓ clean — 2 document(s), 0 hard violation(s), 0 warning(s), 0 injected
# report written to .keep/lint.jsonA clean pass. Now add a relation to a document that does not exist:
relations:
- target: ghost-doc
type: inspired_byRun keep lint again:
keep lint
# lint: ✗ violations found — 2 document(s), 1 hard violation(s), 0 warning(s), 0 injected
#
# ragu [published]
# ✗ dangling_slug: target 'ghost-doc' does not exist
# ✗ invalid_promotion: 'published' requires zero hard violations but 1 foundThe linter exits with code 1 when hard violations are present. This makes it
suitable as a pre-commit gate. Revert the change and the pass is clean again.
Add a supersedes relation to ragu:
relations:
- target: soffritto
type: supersedesRun keep lint:
keep lint
# lint: ✓ clean — 2 document(s), 0 hard violation(s), 0 warning(s), 1 injected
#
# soffritto [canon]
# ↩ injected 'superseded_by' from 'ragu'Keep automatically wrote a reciprocal superseded_by relation into
soffritto.md. Open it and you will see the injected entry marked with
auto_injected: true.
my-wiki/
├── keep.yml # workspace configuration — edit this
├── index.md # generated by `keep index` — do not edit
├── .keep/ # generated artefacts — do not edit
│ ├── graph.json
│ ├── state.json
│ └── lint.json
└── <your documents> # organised however you like
keep.yml is the only file in the workspace you configure directly.
Everything else is either a document you write or an artefact Keep generates.
index.md lives at the workspace root rather than in .keep/ because it is
a first-class navigational artefact — useful to humans and LLMs alike, not just
internal tooling state.
.keep/ contains tool-generated state. Never edit these files manually.
state.json is the only one that is stateful — if you delete it, staleness
timestamps are re-seeded from filesystem mtime.
Document organisation is entirely up to you. Keep walks the workspace recursively, so you can use any folder structure you like. Subfolders by kind, by project, by year — whatever matches how you think.
Every document must have a frontmatter block between --- delimiters at the top
of the file.
| Field | Type | Required | Description |
|---|---|---|---|
slug |
string | yes | Unique identifier. URL-safe, kebab-case. Must be unique across the workspace. |
title |
string | yes | Human-readable document title. |
kind |
string | yes | Semantic document type. Examples: recipe, person, note, prd, experiment. Drives extension selection. |
status |
string | yes | Lifecycle stage. One of: draft, review, published, canon. |
date_created |
ISO 8601 | yes | Creation date. Format: "2026-01-10". |
tags |
list | yes | Freeform tags for discovery. |
summary |
string | no | One sentence describing the document. Used to populate index.md. Intended to be written by an LLM during ingest. |
private |
boolean | no | If true, the document is excluded from index.md and cannot be the target of relations from non-private documents. Default: false. |
relations |
list | no | Typed directed edges to other documents. See below. |
| Status | Meaning | Lint gate |
|---|---|---|
draft |
Raw capture, not yet categorised | None |
review |
Categorised, awaiting assessment | None |
published |
Wiki-quality, active member | Linter must pass |
canon |
Settled, load-bearing, gravitational | Linter must pass |
Documents cannot be promoted to published or canon if the linter reports
hard violations.
relations:
- target: soffritto # slug of the target document
type: derived_from # relation typeAvailable relation types:
| Type | Meaning | Symmetric? |
|---|---|---|
derived_from |
This document originates from the target | No |
extends |
This document builds upon the target | No |
supports |
This document provides evidence for the target | No |
contradicts |
This document conflicts with the target | Yes — auto-injected |
inspired_by |
Loose creative or conceptual influence | No |
supersedes |
This document replaces the target | Yes — auto-injected |
addresses_gap |
This document fills a gap identified in the target | No |
Symmetric relations (contradicts, supersedes) are automatically
reciprocated by keep lint. If document A supersedes document B, Keep injects a
superseded_by relation into B's frontmatter and marks it
auto_injected: true. You should not write superseded_by manually.
Extensions add required fields to documents of a specific kind. Two are built
in:
genealogy — applies when kind: person:
birth_date: "1924-04-15" # required
family_name: Hazan # required
death_date: "2013-09-29" # optionalexperiment — applies when kind: experiment:
hypothesis: "..." # required
outcome: succeeded # required — one of: succeeded, failed, inconclusiveDefine your own extensions in keep.yml. See Configuration.
All commands are run from the workspace root (the directory containing
keep.yml).
Rebuilds .keep/graph.json from frontmatter.
keep graph
# graph written to .keep/graph.json (12 node(s), 8 edge(s))Run after adding or modifying relations.
Updates .keep/state.json with the latest document hashes and timestamps.
keep state
# state written to .keep/state.json (12 document(s))Run after any editing session. Keep distinguishes meaningful changes (status promoted, kind assigned, relations added) from cosmetic ones (prose edits, summary rewrites). Only meaningful changes reset the staleness clock.
Rebuilds index.md at the workspace root.
keep index
# index written to index.md (11 document(s))Private documents are excluded. Run after adding or modifying documents.
Validates all documents and writes .keep/lint.json.
keep lint
# lint: ✓ clean — 12 document(s), 0 hard violation(s), 2 warning(s), 0 injected
# report written to .keep/lint.jsonExits with code 0 if clean, 1 if hard violations are found. Hard violations
block graduation to published or canon. Warnings are advisory.
Hard violations:
dangling_slug— a relation target does not existprivate_target_in_public_doc— a public document relates to a private onemissing_reciprocal— a symmetric relation has no backlink (rare after auto-injection)invalid_promotion— apublishedorcanondocument has outstanding violations
Soft warnings:
incomplete— missing recommended fields or insufficient relationsstale— no meaningful modification within the threshold for the document's status
keep --verbose <command> # enable debug logging
keep --help # list all commandskeep.yml lives at the workspace root. All keys are optional — omitting a
section falls back to the app defaults shown below.
schema_version: 1 # required — must match the installed Keep version
staleness:
draft: { days: 14 } # flag after 2 weeks of no meaningful change
review: { days: 28 } # flag after 4 weeks
published: { days: 180 } # flag after 6 months
canon: { days: null } # never flag canon documents as stale
completeness:
min_ratio: 0.6 # flag if fewer than 60% of recommended fields are set
required_fields:
- kind
- tags
required_relations: 1 # flag if document has no authored relations
extensions:
genealogy:
applies_when: { kind: person }
additional_required:
- birth_date
- family_name
fields:
birth_date: { type: isodate }
death_date: { type: isodate }
family_name: { type: string }
experiment:
applies_when: { kind: experiment }
additional_required:
- hypothesis
- outcome
fields:
hypothesis: { type: string }
outcome: { type: string, enum: [ succeeded, failed, inconclusive ] }You only need to include the sections you want to change. A minimal keep.yml
for a workspace that uses only the defaults:
schema_version: 1Extensions add required fields to documents of a specific kind. The bar for
adding an extension is intentionally high — add one only when the absence of a
field would make a document of that kind structurally incomplete, not just less
useful.
extensions:
recipe:
applies_when: { kind: recipe }
additional_required:
- source
fields:
source: { type: string }
prep_time: { type: string }
servings: { type: string }Keep is designed to work alongside an LLM in a persistent, compounding knowledge base. The intended workflow:
Ingest — when you capture a new source (article, chat export, PDF), ask the
LLM to create a Keep document for it. The LLM writes the frontmatter (slug,
title, kind, tags, summary, relations) and the prose body. The
summary field is specifically for the LLM — one sentence that describes what
the document is, used to populate index.md.
Orient — at the start of every session, the LLM reads index.md first to
locate relevant documents before drilling into them. This replaces expensive
full-workspace scans.
Maintain — run keep state, keep graph, keep index, and keep lint
after each session to keep the derived artefacts current. The LLM can run these
commands itself as part of an ingest skill.
Lint as gate — keep lint exits non-zero on hard violations, making it
suitable as a quality gate before promoting documents to published or canon.
A minimal ingest skill prompt looks like:
Read index.md to orient yourself.
Create a new document for the attached source.
Write frontmatter with slug, title, kind, status: draft, date_created (today),
tags, summary (one sentence), and any relations you can identify.
Run: keep state && keep graph && keep index && keep lint
Report the lint result.
Keep is built with uv.
git clone https://github.com/your-username/keep
cd keep
uv sync --devRun the full check suite:
make check-allIndividual targets:
make py/check # ruff format + lint check
make py/fix # ruff format + lint auto-fix
make py/test # pytest with coverage
make py/security # bandit security analysis
make docs/yml-check # yamllint
make docs/md-check # pymarkdownlntTests require a minimum of 80% coverage. The test suite includes unit tests, integration tests, and subprocess CLI tests.