Keel is a local store for everything about a software project except the code — the specs, the decisions, the tasks, the open questions, the feedback, the reasons. It runs on your machine, an AI coding agent reads and writes it through MCP, and a desktop app shows you what's in it.
If you build software with an AI agent, you have probably noticed two things.
Every session starts from nothing. You explain the project again. What it is, what you decided last week, why the obvious approach doesn't work, what's half-finished. The agent is capable and has no memory, so the explaining never ends and it is never quite the same explanation twice.
What you decide in a conversation evaporates. You spend forty minutes working out that the queue has to be idempotent because retries are at-least-once, you both agree, the code gets written — and the reasoning exists nowhere. Six months later you find the code, can't remember why, and either rediscover the reasoning or quietly break it.
The usual answer is to write it down. In practice that means a wiki nobody updates, a NOTES.md that goes stale in a fortnight, and an issue tracker built for teams of thirty. All of them have the same flaw: keeping them current is a separate chore from doing the work, so it doesn't happen.
The agent writes to Keel while you work, because it is right there in the conversation. No separate step, no context switch, no remembering.
- You mention a constraint → it becomes a decision with the reasoning attached.
- You say "we should probably…" → it becomes a task.
- Something turns out to be undecided → it becomes an open question, and every future session sees it before it re-litigates it.
- The agent finds out why something is slow → that goes on the task as a note, attributed to that conversation.
Next session — different day, different machine, no memory — starts by reading the store and knows where things stand.
It stays yours. Everything lives in ~/.keel on your disk. No account, no cloud, no telemetry. The daemon binds 127.0.0.1 and nothing else can reach it.
It writes readable files. Keel generates markdown into your repository — the spec, the decision log, the tracker, the questions — so the whole thing is greppable, diffable and committed alongside your code. If Keel vanished tomorrow you'd still have the files.
- Not a team tracker. One person, one machine, no permissions, no assignees.
- Not a replacement for GitHub Issues if you have a team using them.
- Not a note-taking app. You don't type into it — you talk to the agent, and the agent writes.
- Not a chat log. It stores what became true, not what was said.
You need Rust and Claude Code.
git clone <this repo> && cd keel./plugin/install.shThat builds the binaries, puts them in ~/.cargo/bin — the same place a release installs, so there is only ever one copy — creates the store at ~/.keel, and copies the agent's skill and hooks to ~/.claude/skills/keel/.
SQLite is compiled in, so the binaries are self-contained — there is no database to install alongside them.
Then start the daemon and leave it running:
keel-daemonIt binds 127.0.0.1:7654. Add --embeddings if you want semantic search as well as keyword search — the first run downloads the model, which takes a minute. Keyword search works either way.
Not every build can do that. The model needs the ONNX runtime, which has no prebuilt Intel macOS library and wants a newer glibc than the Linux binaries are built against, so those builds ship without it and --embeddings has nothing to switch on. It is a cargo feature, on by default, so a build from source has it wherever it links: cargo build --no-default-features leaves it out deliberately. keel doctor and /api/health both report which build you are running — a version number cannot tell you.
Register the MCP server once:
claude mcp add --scope user --transport http keel http://127.0.0.1:7654/mcpThen add the hooks to ~/.claude/settings.json. install.sh deliberately won't edit this file for you — it's yours, and a script rewriting your settings is one bug away from damage:
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "timeout": 10,
"command": "/Users/you/.claude/skills/keel/keel-hook.sh session-start" } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "timeout": 15,
"command": "/Users/you/.claude/skills/keel/keel-hook.sh stop" } ] }
]
}
}The two hooks are what make it work without you asking:
- SessionStart injects a short summary of the project at the top of every conversation, so the agent is oriented before you type anything.
- Stop fires when a session ends having recorded nothing, and asks it to. It stays silent for sessions that already wrote — a prompt that fires when you've done the right thing is a prompt you turn off.
Check it:
curl -s http://127.0.0.1:7654/api/healthEverything has a working default. Override with environment variables:
| Variable | Default | What it does |
|---|---|---|
KEEL_HOME |
~/.keel |
Where the store lives |
KEEL_DAEMON_URL |
http://127.0.0.1:7654 |
Where the CLI looks for the daemon |
KEEL_BIND |
127.0.0.1:7654 |
What the daemon binds |
KEEL_BIN_DIR |
$CARGO_HOME/bin, else ~/.cargo/bin |
Where install.sh puts binaries — the release installer's own default |
KEEL_SKILL_DIR |
~/.claude/skills/keel |
Where the skill and hooks are installed |
KEEL_AUTO_UPDATE |
on | Set to 0 to stop the hourly check for a new release |
One request, and it is worth naming rather than leaving to be discovered: the daemon fetches the latest release manifest once an hour so it can tell you a new version exists. It sends nothing from your store — no project names, no counts, no identifier — and nothing is installed without you agreeing to the restart.
--no-update-check on /keel:setup turns it off at install time,
KEEL_AUTO_UPDATE=0 turns it off afterwards, and keel doctor reports which it
is and when the last check ran. With it off, Keel makes no network requests at
all.
After editing anything under plugin/, re-run ./plugin/install.sh --skill-only. It skips the build and copies the three files. The copies under ~/.claude are what actually run — a change to the repo that isn't copied across does nothing at all.
That's the point. Work with Claude the way you already do. Keel fills up as a side effect.
Some things that make the agent write, without you asking it to:
"Let's go with the second option — Postgres, because we already run one." "That's a bug, the retry loop doesn't back off." "I don't know whether we need per-tenant keys. Leave it for now."
And things that make it read:
"What's the state of the auth work?" "Why did we pick SQLite?" "What's blocking the release?" "What should I do next?"
keel uiThe daemon serves it, compiled into the binary — no Node, no second process, nothing to start. It opens whatever address the daemon is actually listening on, so a non-default port needs no arguments.
A board, a roadmap, documents with revision history, a searchable everything, and an activity feed showing what changed and which conversation changed it.
To work on the interface itself, npm run dev --prefix apps/desktop gives you the usual hot reload against the same daemon.
The app is read-only, on purpose. Claude and the CLI are the only writers. If the app could also write, you'd have two sources of truth for the same row and no answer to which one is right when they disagree.
You rarely need it, but:
keel statuskeel fsckkeel generate <project>keel backupfsck checks referential integrity across both storage engines — 34 checks, and each finding says what it breaks and what to do about it. generate writes the markdown files into your repo. backup writes both engines to Parquet, and restore puts them back.
Everything works whether the daemon is running or not: the CLI asks the daemon when one is there, and opens the store directly when it isn't.
Point a project at your repo and Keel writes markdown into it:
product/SPEC.md the spec, as prose
product/DECISIONS.md every decision, numbered B-1, B-2…, with the reasoning
product/STATUS.md the tracker — open work, rendered from the task rows
product/CHANGELOG.md what closed, and the event log
.keel/questions.md open questions and settled ones, with the answers
.keel/decisions/ one file per decision
.keel/specs/ one file per document
These are outputs. Every one carries a banner saying so. Editing them is not so much wrong as futile — the next keel generate overwrites them from the store, and your words are gone.
To change what they say, change the source: ask Claude to rewrite it, or edit it in the app. If you have already edited a file by hand and want the words kept, keel import <file> writes it back in as a proper revision.
There's a pre-commit hook that refuses a commit carrying a hand-edited generated file, so you find out immediately rather than when the next regeneration eats it:
ln -sf ../../scripts/pre-commit .git/hooks/pre-commitThirteen kinds of thing, and no more — the ceiling is deliberate, because "we need a new type for this" is nearly always a field or a label in disguise:
project, milestone, task, spec, decision, question, term, feedback, design, environment, metric, metric observation, artifact.
They're connected by a typed graph — a task implements a spec, a decision supersedes an earlier one, a task blocks another — and the graph is what makes "what's actually blocked" a query rather than a guess.
The agent sees ten tools: keel_context, keel_search, keel_get, keel_projects, keel_activity, keel_create, keel_update, keel_write_doc, keel_note, keel_link. Ten rather than forty because a model picks the right tool from a short list and the wrong one from a long list.
Talk about the project, don't dictate records. "We're going with Postgres because we already run one" gets you a decision with reasoning. "Create a decision record titled Postgres" gets you a row that says nothing in six months.
Say the reason out loud. The reasoning is the part that has value later — the choice itself is usually obvious in hindsight, and the rejected alternative almost never is.
Use the readable IDs. Tasks are KEEL-42, decisions are B-12. Say those in conversation; they're stable forever and they resolve everywhere an ID is taken.
Let questions be questions. If something is genuinely undecided, having it recorded as an open question is worth more than a confident guess. Every session sees open questions before it starts, which is what stops an agent quietly re-deciding something you already settled.
Don't hand-edit generated files. Change the source. The pre-commit hook will catch you, but the habit matters more.
Run keel fsck occasionally, and keel backup before anything drastic.
One writer. The daemon owns the only write path to the database. Don't run two daemons against one store.
Restart the daemon after upgrading. Keel refuses to start if the binary is older than the store's schema, which turns a silent corruption into an error you can read — but you still have to restart it.
Rust, one workspace, five crates.
crates/keel-core/ domain types, storage, graph, search, generation, backup
crates/keel-mcp/ the thirteen tools, the digest, protocol handling
crates/keel-embed/ the local embedding model, kept out of the core
crates/keel-daemon/ axum: the MCP endpoint and a local read API
crates/keel/ fsck, backup, restore, import, generate, notes —
and both shipped binaries, `keel` and `keel-daemon`
apps/desktop/ Tauri + React. Read and search only
Both binaries are declared by one package because dist builds one installer
per package that owns binaries, and there is meant to be one keel-installer.sh.
The daemon still lives in keel-daemon; only its entry point moved.
Storage is one SQLite file — entities, links, the event log, document revisions, images and vectors all in the same database. Search is hybrid: FTS5 keyword and sqlite-vec similarity, fused by reciprocal rank.
It was two engines until Phase 9: DuckDB for rows and Lance for documents, with the second attached into the first as a SQL namespace. That worked, but it cost a 22-minute build, a keyword index that was rebuilt wholesale on every write, and two backup formats that had to be kept in step. product/SPEC.md D-1 records the original reasoning and what overturned it.
Every change is an event with an author and the conversation that made it, so "who changed this and when" is always answerable.
cargo test --workspacecargo clippy --workspace --all-targets -- -D warningscargo fmt --all --check657 Rust tests and 220 for the app.
All of it is in product/, and all of it is generated from the store:
product/PRD.md— what this is forproduct/SPEC.md— how it worksproduct/DECISIONS.md— every decision and whyproduct/STATUS.md— what's open and what's nextproduct/CHANGELOG.md— what has closed, with the reason and the evidenceproduct/JOURNAL.md— what happened, session by sessionproduct/GATE.md— the one measurement that mattered, and why it stopped