Personal wiki tooling.
Project description
Keep
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.
Contents
- How it works
- Installation
- Getting started
- Workspace structure
- Frontmatter reference
- Commands
- Configuration
- Keep and LLMs
- Development
How it works
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
Installation
Keep requires Python 3.12 or later.
From PyPI (once published):
uv tool install keep
From source (current):
uv tool install git+https://github.com/your-username/keep
Verify the installation:
keep --help
To upgrade:
uv tool upgrade keep
# or from source:
uv tool install --force git+https://github.com/your-username/keep
Getting started
This walk-through takes you from an empty directory to a linted, indexed wiki in a few minutes.
1. Create a workspace
mkdir my-wiki
cd my-wiki
2. Add keep.yml
Every 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: 1
That is all you need to start. See Configuration for the full list of options.
3. Write your first document
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)
4. Run keep state
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.
5. Run keep graph
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.
6. Add a second document with a relation
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.
7. Rebuild the graph
keep graph
# graph written to .keep/graph.json (2 node(s), 1 edge(s))
Two documents, one relation.
8. Run keep index
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.
9. Run keep lint
keep lint
# lint: ✓ clean — 2 document(s), 0 hard violation(s), 0 warning(s), 0 injected
# report written to .keep/lint.json
A clean pass. Now add a relation to a document that does not exist:
relations:
- target: ghost-doc
type: inspired_by
Run 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 found
The 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.
10. See reciprocal auto-injection
Add a supersedes relation to ragu:
relations:
- target: soffritto
type: supersedes
Run 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.
Workspace structure
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.
Frontmatter reference
Every document must have a frontmatter block between --- delimiters at the top
of the file.
Core fields
| 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. |
Lifecycle statuses
| 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
relations:
- target: soffritto # slug of the target document
type: derived_from # relation type
Available 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
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" # optional
experiment — applies when kind: experiment:
hypothesis: "..." # required
outcome: succeeded # required — one of: succeeded, failed, inconclusive
Define your own extensions in keep.yml. See Configuration.
Commands
All commands are run from the workspace root (the directory containing
keep.yml).
keep graph
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.
keep state
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.
keep index
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.
keep lint
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.json
Exits 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
Global flags
keep --verbose <command> # enable debug logging
keep --help # list all commands
Configuration
keep.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: 1
Adding a custom extension
Extensions 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 and LLMs
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.
Development
Keep is built with uv.
git clone https://github.com/your-username/keep
cd keep
uv sync --dev
Run the full check suite:
make check-all
Individual 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 # pymarkdownlnt
Tests require a minimum of 80% coverage. The test suite includes unit tests, integration tests, and subprocess CLI tests.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keep_base-0.1.0.tar.gz.
File metadata
- Download URL: keep_base-0.1.0.tar.gz
- Upload date:
- Size: 25.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
908d87045dd94e018a4359d22516e19482f0add5117a8a26361002fe62dc9ca6
|
|
| MD5 |
b669be643d96e9dda514af79027eb1fa
|
|
| BLAKE2b-256 |
0b9d3dbb32fdb96b52659f07bfe90ec94953aedd73fe2cb02277598b7df8607e
|
Provenance
The following attestation bundles were made for keep_base-0.1.0.tar.gz:
Publisher:
publish.yml on sidisinsane/keep-base
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keep_base-0.1.0.tar.gz -
Subject digest:
908d87045dd94e018a4359d22516e19482f0add5117a8a26361002fe62dc9ca6 - Sigstore transparency entry: 1339596269
- Sigstore integration time:
-
Permalink:
sidisinsane/keep-base@99cdfd7bfeff71e01a556c1ce54835270ba3e0ba -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/sidisinsane
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99cdfd7bfeff71e01a556c1ce54835270ba3e0ba -
Trigger Event:
push
-
Statement type:
File details
Details for the file keep_base-0.1.0-py3-none-any.whl.
File metadata
- Download URL: keep_base-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da91f3b112415454f518506cb9fc8afe5c065f0f15f3d5698b5b4b5baf7b8240
|
|
| MD5 |
f309ee5ad84ebb4b7c7d34deb67d6e7d
|
|
| BLAKE2b-256 |
caf86be9ffd2d80c583018e92304602494ae902b363537fa920749c97c87110f
|
Provenance
The following attestation bundles were made for keep_base-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on sidisinsane/keep-base
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keep_base-0.1.0-py3-none-any.whl -
Subject digest:
da91f3b112415454f518506cb9fc8afe5c065f0f15f3d5698b5b4b5baf7b8240 - Sigstore transparency entry: 1339596297
- Sigstore integration time:
-
Permalink:
sidisinsane/keep-base@99cdfd7bfeff71e01a556c1ce54835270ba3e0ba -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/sidisinsane
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99cdfd7bfeff71e01a556c1ce54835270ba3e0ba -
Trigger Event:
push
-
Statement type: