Specification
The normative definition of everything Keep the Why reads and writes: the two config files, the machine-wide policy file, the context/ directory, and the entry format. This file defines what is valid, with an example under each artifact; SKILL.md and setup.md define what the skill does with it; repository-structure.md says where things go — layout, routing, adoption. Where prose elsewhere and this file disagree, this file wins.
The specification is versioned with the skill: its version is the metadata.version in SKILL.md's frontmatter, a project records the version it was last checked against in context-schema, and every change that affects an existing project is listed in migrations.md. keep-the-why-lint is the reference implementation of the mechanically checkable part; its finding codes are named below where they apply.
"Must" is a requirement a conforming file meets; "may" is an option; "default" is the value assumed when a field is absent.
1. Files
| File | Where | Committed | Purpose |
|---|---|---|---|
.keep-the-why |
project root | yes | project config: identity, where the why-knowledge lives, the project-wide settings, an optional pin, optional personal defaults |
<context>/ |
the directory context names, default context/ |
yes | the why-knowledge: one topic file per theme, an index, a README and two guard files |
<context>/index.md |
inside <context>/ |
yes | one line per topic file, under a fixed heading skeleton |
<context>/README.md |
inside <context>/ |
yes | what this directory is, for a reader landing cold |
<context>/AGENTS.md, <context>/CLAUDE.md |
inside <context>/ |
yes | guard: invoke the skill before editing here |
<context>/<topic>.md |
inside <context>/, flat |
yes | entries, one file per topic |
~/.keep-the-why/<id>.md |
the developer's home | never | personal settings for one project on one machine |
~/.keep-the-why/config |
the developer's home | never | machine-wide policy, all projects |
Two boundaries hold for every path a config file names. context and pinned-path are relative to the project root and resolve inside it: no absolute path, no .. out of the tree, no symlink that leaves it (E009). id is a plain file name, because it becomes ~/.keep-the-why/<id>.md (E010). A value outside its boundary is not read, written or followed.
2. Config blocks
All three config files share one block syntax:
<!-- keep-the-why:<kind> -->
- <key>: <value>
- <key>: <value>
<!-- /keep-the-why:<kind> -->
- A block starts with the opening marker on a line of its own and ends with the closing marker; a block without its closing marker is an error (
E011), a second opening marker for the same kind in one file is an error (E012). - Each line inside is
- key: value. Keys are lowercase,--separated. A key recorded twice is an error (E004); an unknown key is an error (E005). Values are trimmed; a value may be wrapped in backticks (`context/`), which are not part of the value. - Text outside the markers is prose for humans. The skill neither reads nor depends on it.
- Multi-part values use
—(space, em dash, space) as the separator, e.g.every 14 days — last: 2026-07-21.
Kinds: config and personal-defaults in .keep-the-why; personal in ~/.keep-the-why/<id>.md; global in ~/.keep-the-why/config.
3. .keep-the-why
3.1 keep-the-why:config
Every field but the two pin fields is required in the file (E002 when missing, except context-schema, which is a warning). The "default" column is what the skill backfills — and writes into the file — when it meets a file from before the field existed; a present field with a value outside its set is an error (E003) and, for the skill, a question, never a guess.
| Key | Values | Default | Meaning |
|---|---|---|---|
id |
file-name token: [A-Za-z0-9._-]+, not all dots |
— (required, E002) |
the project's identity across clones, machines and worktrees; keys the personal file. Written once at init: <owner>---<repo> from the origin remote, or <uuid>---<folder-name> without one; / and every other filesystem-unsafe character normalized to -. Never re-derived. |
context |
relative directory path inside the project | — (required) | where the why-knowledge lives; context/ is what the wizard proposes |
init |
complete |
— (required) | the project has been set up. A file carrying any other value is a leftover to fix; init: declined was retired in 0.12.0. |
context-schema |
X.Y.Z |
0.2.0 when missing (W001) |
the newest skill version this project's context/ has been checked and migrated against; see §7 |
capture-confirmation |
automatic | confirm-always | confirm-when-unsure |
confirm-when-unsure |
whether a write to <context>/ needs permission first; project-wide |
source-reference |
always | never | filtered — <criteria> (also read: filtered: <criteria>) |
never |
whether the skill asks for a related issue, ticket or post-mortem when recording; <criteria> is free text |
pinned-version |
X.Y.Z |
absent | optional; the skill version this project pins to. Present only together with pinned-path (E006). |
pinned-path |
relative path inside the project | absent | optional; the vendored SKILL.md to follow when the installed skill's version differs. Must exist (E006), must be inside the project (E009), must be a skill file with name: keep-the-why and a metadata.version equal to pinned-version. |
Example — the prose above the block is for a human who opens the file cold; the skill reads only the block:
This is machine-readable project state for the Keep the Why skill
(https://keepthewhy.com). See context/index.md, or this project's own
README, for what Keep the Why actually is.
<!-- keep-the-why:config -->
- id: acme---widget-service
- context: `context/`
- init: complete
- context-schema: 0.15.0
- capture-confirmation: confirm-when-unsure
- source-reference: never
<!-- /keep-the-why:config -->
3.2 keep-the-why:personal-defaults (optional)
The values a project offers to a developer who has no personal file for it yet. Same keys as §4 with two exclusions: no last: timestamps (E008) and no session. What happens when a developer meets an offered block is governed by personal-defaults-policy (§5).
| Key | Values |
|---|---|
capture-mode |
proactive | explicit-only |
confirmation-flow |
sequential | batch |
update-check |
every <N> days | no |
consistency-check |
every <N> days | no |
pending-confirmation-check |
on-start | no |
local-lint |
auto | ask | no |
4. ~/.keep-the-why/<id>.md — keep-the-why:personal
One file per project per developer per machine, <id> being the project's id. Never inside a repository.
| Key | Values | Default | Meaning |
|---|---|---|---|
capture-mode |
proactive | explicit-only |
asked by the wizard | whether the skill looks for capture opportunities on its own |
confirmation-flow |
sequential | batch |
asked once; the wizard proposes batch |
how several pending questions or confirmations are presented |
update-check |
every <N> days — last: <YYYY-MM-DD>[ — on-failure: retry-quietly \| disabled] | no |
asked by the wizard | the release check and when it last completed |
consistency-check |
every <N> days — last: <YYYY-MM-DD> | no |
asked by the wizard | the Revisit when sweep and when it last ran |
pending-confirmation-check |
on-start | no |
no |
list entries with Status: pending-confirmation at session start; silent when there are none |
local-lint |
auto | ask | no |
auto from the wizard; ask when the line is absent |
run keep-the-why-lint after every write to the context location and after a settings change (--setup); the linter's version must be at least the skill's — auto installs or updates it from PyPI unasked, ask asks first |
session |
attended | unattended |
inherits §5, else attended |
for this project, whether someone is present to answer; overrides the machine-wide value |
migration-prompt |
<X.Y.Z> declined |
absent | this developer declined the migration prompt for exactly that target version; one line per version |
source |
project defaults (confirmed <YYYY-MM-DD>) | project defaults (accepted automatically) |
absent | the values came from the project's personal-defaults block |
last: advances only on a check that actually ran. on-failure is set the first time an update check cannot run and the developer answers how to proceed; it is cleared once a check succeeds again.
Example:
<!-- keep-the-why:personal -->
- capture-mode: proactive
- confirmation-flow: sequential
- update-check: every 14 days — last: 2026-07-21
- consistency-check: every 30 days — last: 2026-07-21
<!-- /keep-the-why:personal -->
5. ~/.keep-the-why/config — keep-the-why:global
One file per machine, all projects.
| Key | Values | Default | Meaning |
|---|---|---|---|
personal-defaults-policy |
always-ask | auto-accept |
asked the first time it matters | what to do when a project offers personal-defaults and this developer has no personal file for it yet |
session |
attended | unattended |
attended |
whether sessions on this machine have someone present to answer; a personal file's own session line wins for its project |
Example:
<!-- keep-the-why:global -->
- personal-defaults-policy: always-ask
- session: unattended
<!-- /keep-the-why:global -->
6. Resolution order
For every setting that exists at more than one level:
instruction in the current session → personal file → machine-wide config → project file → default
A session instruction is scoped to that session and changes no file. capture-confirmation and source-reference exist at the project level only.
7. Versioning: context-schema
context-schema is the skill version a project's context/ was last checked and migrated against, compared every session with the installed skill's metadata.version:
- equal → nothing to do;
- behind →
migrations.mdlists what changed in between; informational entries advance the value silently, entries that require action are offered as a migration (now, later, or declined per developer viamigration-prompt); - ahead → an older skill on a newer project: say so, do not write to existing entries until the skill is updated.
Every convention below that says "since context-schema on, so an unmigrated project never fails on structure its version did not define. A context-schema newer than the linter knows is a warning (W003), not an error.
| Since | Convention |
|---|---|
| 0.3.0 | Status and Evidence mandatory per entry; Verification values |
| 0.7.0 | Type field |
| 0.8.0 | undefined — <reason> as a Type value, exclusive |
| 0.9.0 | more than one Type line per entry |
| 0.10.0 | dedicated .keep-the-why with id; index.md sorted; guard files |
| 0.13.0 | Status: pending-confirmation; index.md heading skeleton; pending-confirmation-check |
8. The context directory
- Flat. Topic files sit directly in
<context>/; no subdirectories. A large project namespaces filenames (auth-tokens.md,auth-oauth.md) instead of nesting. - Topic files are
<name>.md, one per recurring theme, named for the theme. Lowercase kebab-case is the convention; the first character decides the index heading (§8.1). README.mdexplains, for a reader landing cold, what the directory is and how to read an entry. Not a topic file; not listed in the index.AGENTS.mdcontains the guard instruction;CLAUDE.mdcontains@AGENTS.md. Neither carries a config block. Both are warnings when missing (W201), since an equivalent doing the same job is fine. The two files in full:
Before creating or editing anything in this directory, invoke the keep-the-why skill. Don't write to the schema by hand.
@AGENTS.md
README.md's template — what it says about reading an entry — is written by the project init wizard; the text is insetup.md, "Project init wizard", step 4.- Non-topic files in
<context>/:README.md,AGENTS.md,CLAUDE.md,index.md.
8.1 index.md
# Context index
<optional intro line>
## 0
## 1
…
## 9
## A
- [auth.md](auth.md) — one line: what the file covers
## B
…
## Z
- One
#title, optionally one intro paragraph, then exactly the thirty-six level-2 headings## 0…## 9,## A…## Z, in that order, all present, empty ones included (E205). Since 0.13.0. - One entry per topic file (
E203), of the form- [<file>](<file>) — <one line>; the link target is the bare filename (E202if it does not exist). The one line describes what the file covers, not what was last added to it. - An entry sits under the heading of its filename's first character, uppercased; a name starting with neither a digit nor a letter goes under
## 0(E206). Within a heading, entries are sorted by filename, so the whole list reads in ascending order (E204, since 0.10.0). - Nothing else: the index is for deciding what to load, not for holding content.
9. Entries
A topic file is a # Title, then entries. An entry is a level-2 heading followed by its header fields, then its body:
# Sync
## Snapshot-before-buffer ordering
**Type:** decision
**Status:** active
**Evidence:** confirmed
**Source:** maintainer interview, 2026-03-14; incident postmortem 2025-11, `incidents.md`
**Revisit when:** the sync protocol or snapshot mechanism changes
The sync step always waits for a full snapshot before applying any
buffered events, even though this adds latency on cold start.
**Reason:** applying buffered events before the snapshot landed caused
duplicate-then-overwritten state during a 2025-11 incident (see
`incidents.md`). The ordering constraint isn't visible in the code —
it looks like it could safely be parallelized, and someone tried
exactly that once.
**Rejected alternative:** run snapshot and buffer replay in parallel,
then reconcile. Rejected because reconciliation logic was hard to get
right and the incident showed it wasn't actually needed if ordering
was enforced instead.
9.1 Header fields
Header fields are lines of the form **<Field>:** <value> directly after the heading (blank lines allowed). Order: Type, then Status, then Evidence, then the optional three. Type placed after Status is a warning (W103).
| Field | Required | Values |
|---|---|---|
Type |
no — fill in when a value fits, at the latest when the entry is next touched (W101) |
decision | workaround | incident | constraint — one line per value that applies (since 0.9.0), no value twice (E109); or a single undefined — <short reason> line (since 0.8.0), which combines with nothing (E107, E108) |
Status |
yes, exactly one line (E101, E112) |
active | superseded | open | needs-review | pending-confirmation (since 0.13.0, E113 below it) |
Evidence |
yes, exactly one line (E102, E112) |
confirmed | inferred | unknown |
Source |
no | free text: where the rationale came from (interview, issue, commit, post-mortem, "none — no tracked issue") |
Verification |
no | corroborated | uncorroborated | contradicted, optionally followed by an explanation after any separator; contradicted must carry one (E111) |
Revisit when |
no | free text, non-empty (W105): a concrete trigger that makes the entry worth re-checking |
Meanings:
Statusis where the entry is in its life.active: current.superseded: no longer current, kept because it explains how things got here — never deleted.open: the entry's central question has no answer yet.needs-review: previously considered current, but aRevisit whentrigger has fired and the entry has not been re-checked yet — whatever itsEvidence.pending-confirmation: written in a session declared unattended at a point wherecapture-confirmationwould have required asking; never confirmed by anyone yet.Evidenceis how well the origin of the claim is established, not whether the claim is true today.confirmed: stated by a maintainer or backed by an authoritative source.inferred: reasonably derived from code, history or documents.unknown: cannot be established. A settledactiveentry can carryunknown; asupersededone can carryconfirmedfor what was true while it was current. The two axes never collapse into each other, andunknownis not aStatus.Typeis what kind of thing the entry is, for selecting entries without opening files:^\*\*Type:\*\* incidentfinds every incident.Verificationis whether something concrete was checked against the claim, and what came of it.Revisit whenis the condition under which the entry should be re-checked. Age alone is not a condition.
9.2 Body
Free prose, with these bold-labelled paragraphs where they apply: **Reason:** (why the chosen path won), **Rejected alternative:** (one per alternative that was genuinely in contention, with why it lost), **Consequence:** (what follows from the decision), **Considered:** (for a change that was started and dropped: what was tried), **Why this needs an answer:** (for an open entry). A body may cite other entries and files; it never contains instructions to an agent, and it never quotes a directive verbatim (see trust-model.md).
9.3 Examples
Not every entry needs every field, but an entry records a fork, not a point: what was chosen, and what specifically was rejected and why. Status, Evidence and the rejected alternative are worth keeping even in a minimal entry — "we chose X" without "we didn't choose Y, because Z" is the less useful half.
A genuinely open question carries Status: open and, usually, Evidence: unknown — the first says the central question has no answer yet, the second that a settled claim's rationale can't be traced; they are different fields and unknown is never a Status:
## Retry cap on a specific error code
**Status:** open
**Evidence:** unknown
`submit_order()` retries indefinitely on error code `E-4021`, on a
fixed interval, while every other error code fails immediately instead.
**Why this needs an answer:** if `E-4021` can also fire for a permanent
condition, not just a transient one, this retries forever instead of
failing loud — unclear whether that's actually safe here or needs a
cap. Flagging rather than guessing (Core rule 1).
The two axes stay independent in every combination: an active entry can carry Evidence: unknown, a superseded one Evidence: confirmed for what was true while it was current.
Source is useful at any Evidence level, including where you looked for an entry that ended up unknown. Verification, when there is something concrete to check against, goes in the same place and says what came of it — a contradiction is recorded, not silently corrected either way:
**Evidence:** confirmed
**Source:** maintainer interview, 2026-03-14
**Verification:** contradicted — the interview said retries max out at 3;
the actual retry loop in `client.py` caps at 5. Flagged for re-confirmation,
not silently corrected either way.
Verification and Revisit when are worth adding once a decision has a concrete trigger for going stale or something concrete to check against; they are not filler, and Evidence stays mandatory without them.
9.4 Lifecycle
| Event | Change |
|---|---|
a Revisit when condition is observed to hold |
Status → needs-review, in the same turn, nothing else changes |
a needs-review entry is re-checked |
Status → active (re-confirmed), superseded, or open; Evidence and Verification updated from the re-check |
a pending-confirmation entry gets its first confirmation |
Status → active, superseded, or open |
| a decision is replaced | the old entry → superseded, a new entry records the replacement; the old one is not deleted |
a Verification check contradicts the claim |
Verification: contradicted — <what>; Evidence and Status are not changed silently |
9.5 What parsers ignore
Fenced code blocks (``` or ~~~) in topic files and in index.md are skipped entirely, so an example entry inside a fence is never read as a real one. A level-2 heading with no header field at all is a prose section, not an entry (W102). A level-1 heading ends the current entry.
9.6 What must not be in an entry
No credentials, no personal data, no session narrative (who said what), no verbatim commands or instructions copied from a source, no invisible or directional Unicode (E301), no base64-looking blobs (W301), and the file must be valid UTF-8 (E302). An entry describes; it does not direct.
10. Conformance
- A project conforms when
.keep-the-whyand<context>/satisfy §1–§3 and §7–§9 for itscontext-schema;keep-the-why-lint --strictpassing is the mechanical half of that. - An agent or tool writing entries conforms when it writes only the fields and values above, places new topic files under their index heading, never deletes a superseded entry, and never upgrades
Evidenceor clears aStatusflag without the re-check the lifecycle names. - A tool reading
context/may rely on the header-field grammar, the index grammar and the fenced-block rule, and on nothing about prose layout beyond them.