Cuaderno
Cuaderno (cdno) is a command-line vault manager for the Research Logbook Method (RLM) — a
way of running long-horizon knowledge work out of a folder of plain Markdown files. It is built for
researchers and other deep-work practitioners, and is deliberately friendly to ADHD working styles:
it leads with what is there, not what is missing — no guilt counters, no angry overdue badges,
explicit permission to park or drop work.
You drive it from a terminal, and an AI assistant (Claude and other MCP clients) can drive it too, through the bundled MCP server.
What it gives you
- A dated, append-only log as the single source of truth for what you did.
- Projects with a current state, next actions, milestones, and waiting-on items — capped at five active at a time so you can’t overcommit.
- Evidence portfolios: per-question dossiers that accumulate papers, experiment results, and notes over months and years.
- Important questions kept front-and-centre and re-read often.
- Stewardships: small, bounded, long-haul responsibilities (health, finances) with optional habit tracking.
- A commitments register — promises with dates, aggregated from everywhere they live.
- Full-text search, frontmatter linting, and a JSON mode so every read and write verb is scriptable.
Two ways to use it
| Surface | What it is | Start here |
|---|---|---|
cdno CLI | The terminal tool for the daily loop | Quickstart |
cdno-mcp server | An MCP server so Claude can read and write your vault | Connect to Claude |
Both operate on the same Markdown vault — the files on disk are always the source of truth (the SQLite index is just a rebuildable cache).
How this guide is organised
- Getting started — install, create a vault, run the daily loop, and wire up Claude.
- Concepts — the mental model: the method, the twelve note types, the vault layout, and the rules the tool enforces.
- Tutorials — task-oriented walkthroughs of each workflow.
- CLI reference — every command, flag, and example.
- MCP server reference — every tool exposed to AI clients.
- Appendix — JSON shapes, frontmatter fields, the full config file, recurrence syntax, and troubleshooting.
This guide documents the shipped behaviour of Cuaderno. New to it? Read The Research Logbook Method for the why, then jump to the Quickstart for the how.
Installation
Cuaderno ships two binaries:
cdno— the command-line tool for the daily loop.cdno-mcp— the MCP server that lets Claude (and other MCP clients) read and write your vault.
Both are installed together.
Homebrew (macOS and Linux)
The recommended path. Pre-built bottles exist for macOS (Apple Silicon + Intel) and Linux (x86_64 + aarch64).
brew install agustinvalencia/tap/cuaderno
Verify:
cdno --version # -> cdno 0.1.24 (or newer)
cdno-mcp --version
To upgrade later:
brew upgrade cuaderno
From source
Use this on platforms without a bottle, or to track the latest main. You need a
Rust toolchain.
git clone https://github.com/agustinvalencia/cuaderno
cd cuaderno
cargo build --release --bins
# Binaries land at target/release/cdno and target/release/cdno-mcp
Put them on your PATH — for example:
ln -s "$PWD/target/release/cdno" /usr/local/bin/cdno
ln -s "$PWD/target/release/cdno-mcp" /usr/local/bin/cdno-mcp
Shell completions (optional)
cdno can print a completion script for your shell, with vault-aware suggestions (it completes
project, portfolio, and stewardship slugs by re-invoking the binary on TAB):
# zsh — add to your ~/.zshrc:
source <(cdno completions zsh)
Supported shells: bash, zsh, fish, elvish, powershell. See
completions for per-shell setup.
Uninstall
brew uninstall cuaderno # if installed via Homebrew
# or, for a source build, remove the symlinks you created:
rm -f /usr/local/bin/cdno /usr/local/bin/cdno-mcp
Uninstalling the binaries never touches your vault — it’s just Markdown files on disk. Delete the vault directory yourself if you want it gone.
Next step
Create your vault: Initialise a vault.
Initialise a vault
A vault is just a directory of Markdown files with a .cuaderno/ config folder at its root.
Create one with cdno init:
cdno init ~/notebook
cd ~/notebook
This scaffolds the full folder tree, writes a default .cuaderno/config.toml (which declares the
concept type), and writes two starter templates into
.cuaderno/templates/: daily.md and concept.md. Every other note type renders from its built-in
template until you add a file for it there. What you get:
~/notebook/
├── journal/ # daily + weekly notes, partitioned by year
│ └── 2026/ # e.g. journal/2026/daily/2026-04-25.md, journal/2026/weekly/2026-W17.md
├── projects/ # project maps (max 5 active)
│ ├── _parked/ # inactive projects
│ └── _done/ # closed projects, partitioned by year
├── actions/ # manifest action notes (the heavy form)
│ └── _done/ # completed actions, partitioned by year
├── portfolios/ # evidence dossiers, one folder per question
├── stewardships/ # long-haul responsibilities (flat file or folder)
├── commitments/ # standalone promises with deadlines
│ └── _done/ # fulfilled commitments
├── questions/
│ ├── research/
│ └── life/
├── inbox/ # quick captures awaiting triage
├── concepts/ # the concept library (a custom type init declares)
└── .cuaderno/
├── config.toml # vault configuration
└── templates/ # note templates (override the built-ins here)
See Vault structure for what each folder holds.
Running cdno from anywhere
You rarely need to be at the vault root. cdno finds the vault by walking up from your current
directory until it sees a .cuaderno/ folder — so commands work from any subdirectory.
When you’re outside any vault, two fallbacks apply, in order:
- The
--vault <PATH>flag (highest priority — overrides everything). - The
CUADERNO_VAULT_PATHenvironment variable.
# From anywhere, target a specific vault:
cdno --vault ~/notebook log "spotted a bug in the sampler"
# Or set it once for the shell session:
export CUADERNO_VAULT_PATH=~/notebook
cdno log "spotted a bug in the sampler"
If you’re standing inside vault A while
CUADERNO_VAULT_PATHpoints at vault B, the directory you are in wins — writes land in A. The env var is only a fallback for when discovery finds nothing.
Upgrading an older vault
cdno init refuses to run on a vault that already exists: re-initialising is destructive, so it is
never done for you. When a newer cdno ships a note type that init now declares, add it to your
existing vault instead:
cdno config note-type install --list # what ships, and whether this vault has it
cdno config note-type install --name concept
It writes the type’s template, its folder and its declaration, each only when absent. A
declaration you already have, or a template you have edited, is kept and reported, never
overwritten, so the command is safe to re-run; it ends with concept: already installed, nothing to do when there is nothing left to write. Add --dry-run to see exactly what it would write first.
See Installing a bundled note type.
Back up your vault
A vault is just Markdown files (the SQLite index in .cuaderno/index.db is a rebuildable cache — see
Business rules). The simplest, most durable backup is version
control: git init the vault and commit as you go, or keep it in a synced folder.
cd ~/notebook
git init && git add . && git commit -m "Initial vault"
# The index is regenerated on demand, so it's safe to ignore:
echo ".cuaderno/index.db" >> .gitignore
Because the Markdown is the source of truth, your history is just your commits — nothing is locked inside a proprietary store.
Next step
Run your first daily loop: Quickstart.
Quickstart: the daily loop
This is Cuaderno in five commands. It assumes you’ve installed cdno and
created a vault. Run these from inside the vault (or with --vault).
# 1. Start a project (capped at 5 active).
cdno project create --title "Surrogate model" --context work
# 2. Give it a next action, tagged by the energy it needs.
cdno action add --project surrogate-model \
--title "Run feature set B on the full mesh" \
--energy deep
# 3. Record a promise with a deadline.
cdno commit create --title "Pay rent" --due 2026-06-01 --context personal
# 4. Each morning: see what's due, your active projects, and a suggested start.
cdno orient --energy deep
# 5. Mark the action done when it's finished (substring match on the bullet).
cdno action complete --project surrogate-model --query "feature set B"
Throughout the day, the two verbs you’ll reach for most:
cdno log "scaled the mesh to 2M cells; runtime 4x, still stable" # append to today's log
cdno capture "check whether Chen 2025 used the same preconditioner" # drop a thought into the inbox
Interactive vs. scripted
Every write command follows the same convention (see CLI overview):
- In a terminal, omit a required flag and
cdnoprompts you for it, then asks you to confirm before writing. - In a script, pipe, or with
--no-interactive, a missing required flag is an error instead — so automation never blocks on a prompt.
# Interactive: cdno asks for the title and context.
cdno project create
# Scripted: everything supplied, no prompts.
cdno project create --title "Surrogate model" --context work --no-interactive
Machine-readable output
Add --json to any read or write verb to get structured output instead of a formatted table.
Read verbs emit their listing/detail; write verbs emit a { "path": ..., "message": ... } result and
run non-interactively. Great for scripts and for piping into jq.
cdno project list --json | jq '.[].slug'
cdno project create --title "Surrogate model" --context work --json
# -> { "path": "projects/surrogate-model.md", "message": "Created projects/surrogate-model.md" }
See JSON output for the full shapes.
Where to go next
- Understand the model: The Research Logbook Method.
- Work through each workflow: Tutorials.
- Let Claude drive: Connect to Claude.
Connect to Claude (MCP)
Cuaderno ships an MCP server, cdno-mcp, that exposes your vault
to AI clients — Claude Desktop, Claude Code, Kiro, Gemini CLI, and anything else that speaks MCP.
The assistant can then read context (your orientation, a project, a portfolio) and make writes
(append to the log, file evidence, complete an action) on your behalf.
It’s the same engine as the CLI — the files on disk stay the source of truth.
Register the server
Add cdno-mcp to your client’s MCP configuration. For Claude Desktop, edit
~/.claude/claude_desktop_config.json (Claude Code: ~/.claude.json or per-project .mcp.json):
{
"mcpServers": {
"cuaderno": {
"command": "cdno-mcp",
"env": { "CUADERNO_VAULT_PATH": "/Users/you/notebook" }
}
}
}
commandmust resolve on the client’sPATH. If it doesn’t, use the absolute path (e.g./opt/homebrew/bin/cdno-mcp, ortarget/release/cdno-mcpfor a source build).CUADERNO_VAULT_PATHtells the server which vault to open. You can omit it; the server then opens whichever vault its working directory belongs to (the same discovery rule as the CLI).
Restart the client. Cuaderno’s tools then appear in the client’s tool list.
What the assistant can do
The server advertises 66 tools, grouped by purpose:
- Context-gathering reads —
get_orientation,get_project_context,get_portfolio_contents,get_weekly_context,search_notes,read_daily_note, and more. - Writes —
append_to_log,file_to_portfolio,update_project_state,add_action,complete_action,create_commitment,create_tracking_entry, the daily/weekly section writers, and others. - Creation and lifecycle —
create_project,create_portfolio,create_question,create_stewardship,park_project,activate_project,set_question_status, and so on.
The full catalogue, with each tool’s inputs and output shape, is in the MCP server reference.
Skills
You can wrap common multi-step flows as Claude skills that call these tools — e.g. a daily
orientation that reads get_orientation and writes your intention with upsert_daily_section. The
repo ships worked examples under
examples/skills/; see
Using with Claude skills.
Next step
Learn the model behind it all: The Research Logbook Method.
The Research Logbook Method
The Research Logbook Method (RLM) is the practice Cuaderno implements. It distils habits common to prolific researchers — Faraday’s notebooks, Darwin’s dated entries, Hamming’s “important problems,” Knuth’s and Tao’s working logs — into eight concrete practices across two tracks (seven in RLM 1.0; see Method version). Each maps onto one or more note types the tool manages.
Two tracks
Research is two jobs at once. Inquiry is open-ended, driven by questions and evidence. Operations is delivery — deadlines, collaborators, promises. Most systems handle one and force the other into its shape. The RLM keeps them separate and lets them share a substrate: the daily log, the weekly review, and one set of projects that bridges the two.
Track 1 — Inquiry: how you investigate
- A chronological log (Faraday). A single append-only stream of what you did, observed and thought, in the order it happened. Past entries are never edited; a change of mind is a new entry referencing the old one. The one exception is an address: when a page is renamed, every reference to it is corrected to the new name, the old name is kept on the page, and the rename is itself an entry. It removes the “where do I put this?” decision, preserves the reasoning and not just the result, and keeps you honest against hindsight. Unified across contexts — the regularisation weight and the plumber go in the same day.
- Evidence portfolios (Darwin). One folder per active question, accumulating whatever is relevant. The log interleaves many questions; the portfolio aggregates one. Name the folder practically, but phrase the question at the top of its index: “sparse models” names a topic, “does the sparse variant outperform the dense baseline out of distribution?” tells you when you are done. Don’t organise inside — accumulate, and file as things arise, while your reaction is still the most valuable part.
- Important questions (Hamming). The few questions that would genuinely change your situation if answered. Not tasks — questions. They sit above projects: one question may spawn several over time and persists as they finish or get shelved. Two short lists, research and life, reviewed monthly: are these still the right questions?
Track 2 — Operations: how you deliver
A question and a project are not the same thing. A question is open-ended and may branch or turn out to be the wrong question. A project is bounded — a deliverable, a deadline, people counting on you. One question can span several projects; a project can draw on several portfolios; either can exist without the other. Keeping them apart stops deadline pressure polluting open-ended inquiry, and stops open-ended inquiry dissolving operational commitments.
-
Project maps (Knuth). A mutable one-pager per piece of finite work — the page you open after a two-week gap. It answers: where was I, what do I believe and what would change my mind, what are my next actions, what is time-sensitive, where is everything. Tasks live here, embedded in the project that gives them meaning. Maximum five active projects, at most three next actions each — fifteen tasks as a ceiling, not a backlog. A project map is not a history (the log is), not a long checkbox list (eighteen unchecked boxes reads as failure), and not a timeline.
Parking, clarified (1.1). A parked project map is a complete map. Parking changes two things only: the project does not count against the five, and it is read at the monthly review rather than daily. Everything else a map does, it keeps. You may write next actions on a parked map, update its state, note a milestone, note who you are waiting on. A hard-dated milestone on a parked map, or a dated action on it, is still a promise and stays in the commitments register.
A map you start work on is a map that counts: being in the middle of something is the strongest sign you are moving it yourself. If you find yourself working on a parked map, or picking up work you had set down on it, it is not parked; activate it, parking another if you must.
On adopting this clarification, read the parked maps once: every stale date on them is dropped, with its entry in the log, or re-dated.
-
Stewardships. Dashboards for perpetual responsibilities — health, finances, household — that never finish. The critical distinction: projects end, stewardships do not. If health and finances occupy project slots you permanently lose two of five to things that cannot complete. They run in the background with periodic commitments and get a scan at the weekly review, without competing for slots.
-
A commitments register. One flat, date-sorted list of everything with a hard external deadline, whichever project or stewardship it came from. This is not a to-do list, and the distinction matters: a to-do is something you decided to do, a commitment is something someone else is counting on. Mixed together, the commitments drown and you break promises you meant to keep.
-
Energy-matched scheduling (Tao, Wolfram). Tag work deep, medium or light and match it to your current cognitive state. Forcing deep work when your brain wants something mechanical is not discipline, it is waste — the alternative to deep work on a low day is usually paralysis, not virtue. The exception: if you never have deep days, that is environmental, and the fix is protecting blocks rather than trying harder.
-
Handing off (1.1). A project map answers: where was I, what do I believe and what would change my mind, what are my next actions, what is time-sensitive, where is everything. It gains one more question: whose move is it?
A project you have handed to someone else carries their name at the top of its map: held by: the harbour office, held by: a colleague, held by: the contractor who has the hull. The holder is whoever you could write in Waiting On. A map with no name on it is yours; a map that names you is yours. The page stays where it is; only the name changes.
Why: if a project someone else is moving holds one of your five, the cap counts their attention as yours, and you will either raise the number or stop writing such work down. Both are worse than a name on a page.
The count. Count the maps that are neither parked nor handed off. A handed-off project is still open: it keeps its map, keeps logging, keeps its dates in the register, and is still read at the weekly review, and at the daily orientation when something has come back. It is not a sixth slot; it is a project that is not a unit of your attention this week.
The review. Yours: did it move? Handed off: what came back, and what is waiting on your decision? The second is asked in a week when the map changed.
The log. Handing off and taking back are dated entries in the chronological log naming the project and the holder; taking back strikes the name. Taking back a parked map only strikes the name; to work on it, activate it as well, as its own entry.
The cap is checked whenever a map that did not count begins to count: activating a map nobody else holds, or taking back an active one.
Three guards. If you cannot name who has it, it is yours. A handed-off map says what you are waiting for; a name over an empty Waiting On is a parked map wearing a name, and the weekly review parks it or takes it back. And handing off is never the answer to “the cap is full”: doing a thing on a handed-off map does not make it yours, doing most of its work does; at the weekly review, a handed-off map you worked on yourself on two or more days is taken back, or its name changed to who really has it. You do not hand off a map while you are in the middle of its work.
The asymmetry. Parking is your decision about now, so starting work on a parked map undoes it at once; a holder is a fact about someone else, which one afternoon’s work on the map does not change.
The map already has a Waiting On section for single items. Handing off is Waiting On lifted to the whole project, which is why the method already half-had it. Its ancestry is GTD’s Waiting For, kept per project rather than as a separate list.
The bridge
The log feeds both tracks: an observation becomes evidence in a portfolio, a realisation that something must be rerun before a deadline becomes a next action on a project map. The weekly review governs both — retrospective and forward scan. Projects reference the portfolios they draw on, and commitments decompose into milestones that surface as next actions at the right time.
The daily orientation is where the tracks meet: check what is urgent, glance at what is important, check your energy, and pick one thing.
Actions, not tasks
The RLM speaks of actions, not “tasks,” on purpose. The default form of an action is a single inline bullet on a project map — not a heavyweight per-item note. You only “promote” an action to its own note when it grows into an investigation spanning multiple days and evidence artefacts. This keeps the friction of capturing the next step near zero. See Actions.
Designed for ADHD working styles
The method is deliberately shaped to be sustainable when executive function is unreliable:
-
Leads with what is there, not what is missing — no guilt engine, no red overdue counts.
-
Permission to park or drop. Projects park, complete or drop, questions retire, actions and milestones drop, commitments get fulfilled or dropped — all first-class, and none of it is a failure state.
“Reversible” is worth stating precisely, because it means less than it might sound like. Parking is genuinely two-way: a parked project activates again, and a retired question does too. Dropping is not — there is no un-drop verb for an action, a milestone or a commitment, and none is planned. It is re-decided rather than undone: if a dropped thing turns out to matter, make it again, and the record honestly shows two decisions rather than pretending the first never happened. That is the same reason a drop is logged as a drop and not as a completion.
Projects are the one named exception. A dropped project keeps its slug, its map and its history, so
cdno project activatebrings it back, and a project dropped in error can later be completed, or the reverse. Each is still a new decision with its own log line, never an erasure of the first.What a drop does preserve is the note. A dropped action or commitment keeps its file, its body and its
createddate, archived under_done/<year>/— so the context you wrote survives even though the plan did not. A milestone is only a bullet, with no note behind it, so dropping one leaves just the log entry naming it — including when it had sub-bullets, which is the one place this guarantee does not yet hold (#574). -
Minimal maintenance. If keeping the system running costs more than a few minutes a day outside the weekly review, something is wrong.
-
One obvious next step.
cdno orientanswers “what should I do now?” with a single suggestion, biased to your current energy.
The rhythm
The method runs on a few interlocking loops:
- Daily — orient in the morning, log and act through the day, a light close at the end. See The daily loop.
- Weekly — a short retrospective (wins, challenges, one improvement) and a single goal for next week. See Weekly review.
- Monthly — the parked maps (“is it time?”) and the questions (“still the right questions?”).
- Occasional — file evidence as you find it, triage the inbox, prune questions and projects.
Method version
The version tracks the method’s text, not any tool. A change is minor if every page of an existing logbook can be read under it without being rewritten, moved, or given a line it lacks; a minor version may ask for a one-time read-through of existing pages. A change is major if any page must be rewritten or moved, and it comes with instructions for doing so (in a tool, a migration). A logbook records on its first page, dated, the version it follows and the day it moved to a newer one; the published method keeps a changelog of practices.
Changelog of practices
- 1.0 — the seven practices: the chronological log, evidence portfolios, important questions, project maps, stewardships, the commitments register, and energy-matched scheduling.
- 1.1 — parking clarified (under practice 4), practice 8 (handing off), and the monthly loop in the rhythm. Additive: every 1.0 logbook is a valid 1.1 logbook. On adopting 1.1 a paper user does two things: a dated line on the first page, and one pass over the parked pages’ dates.
- 1.2 — practice 1 gains its one exception: a renamed page’s references are corrected, the old name is kept on the page, and the rename is itself an entry. Additive: every 1.1 logbook is a valid 1.2 logbook. On adopting 1.2 a paper user does one thing: a dated line on the first page; there is nothing to read through.
How it maps to the tool
| Practice | Note type(s) | Primary commands |
|---|---|---|
| Chronological log | daily, weekly | log, orient, review |
| Evidence portfolios | portfolio, evidence | portfolio, file |
| Important questions | question | question, questions |
| Project maps | project, action | project, action |
| Stewardships | stewardship, tracking | stewardship, track |
| Commitments register | commitment (+ computed) | commit, commitments |
| Energy-matched scheduling | (an energy on actions) | orient --energy, action add --energy |
| Handing off | project (a held_by holder) | project hand-off, project take-back |
Read on: Note types.
Note types
Every note in a vault has a type: in its frontmatter. Cuaderno parses that frontmatter into a
typed structure — if it parses, it’s valid. There are twelve note types.
| Type | Lives in | Mutability | Purpose |
|---|---|---|---|
daily | journal/<year>/daily/ | Append-only | One day’s chronological log |
weekly | journal/<year>/weekly/ | Append-only | Weekly review (Wins, Challenges, One Improvement, This Week’s Goal) |
monthly | journal/<year>/monthly/ | Append-only | Monthly review (Wins, Themes, Next Month’s Focus) + links to the month’s weeks |
project | projects/ (+ _parked/, _done/<year>/) | Mutable | Project map: state, next actions, milestones, waiting-on |
action | actions/ → actions/_done/<year>/ | Mutable while open, then archived | Manifest note for an action-as-investigation |
portfolio | portfolios/<slug>/_index.md | Occasionally edited | Index/summary of an evidence dossier |
evidence | portfolios/<slug>/ | Append-only | A single piece of evidence (paper, result, note) |
stewardship | stewardships/ (flat or folder) | Occasionally edited | Dashboard for a perpetual responsibility |
tracking | stewardships/<slug>/tracking/ | Append-only | One time-series entry (a gym session, a measurement) |
question | questions/research/ or questions/life/ | Status transitions | An important research or life question |
commitment | commitments/ → commitments/_done/ | Moves when it ends, kept or dropped | A standalone dated promise |
Plus the inbox: raw, untyped captures in inbox/ awaiting triage.
The journal: daily, weekly, and monthly
The chronological backbone. Daily notes collect timestamped log lines plus structured sections
(Intention, Agenda, Standup, Meeting), and a ## Notes section for worked-out substance that
the log points at (see The concept library).
Weekly notes hold the review (Wins, Challenges, One Improvement) and the week’s single goal.
Monthly notes hold the higher-altitude review (Wins, Themes, Next Month’s Focus) and a ## Weeks
block that links the month’s weekly notes rather than copying them, so the weeks stay the
source of truth. All three are append-only — the historical record only grows.
project — the one mutable map
Projects are the only freely-mutable note type. A project carries a Current State, a list of
next actions (inline bullets by default), milestones, and waiting-on items. When you
update the Current State, the previous state is auto-logged to today’s daily note first, so history
is never lost (see Business rules). At most five projects are active at
once; the rest live parked in projects/_parked/, and a completed or dropped project moves to
projects/_done/<year>/.
action — inline by default, a note when it grows
An action’s default form is a checkbox bullet on its project map. That’s usually all you need. When a
single action becomes an investigation spanning days and artefacts, you promote it to a manifest
action note (heavier: status, energy, criteria, links). Completing an action removes the bullet,
logs it, and — if it had a note — archives that note to actions/_done/<year>/. See
Actions.
portfolio + evidence — dossiers per question
A portfolio is a folder named for a question, with an _index.md (the portfolio note) and a set of
evidence notes filed into it over time. Each evidence note records a source and an origin
(a wikilink to whatever produced it). Evidence is append-only — a portfolio is a growing record. See
Research and evidence.
stewardship + tracking — long-haul responsibilities
A stewardship is a dashboard for something you tend indefinitely. It can be flat (a single
stewardships/<slug>.md) or expanded (a stewardships/<slug>/ folder with _index.md, a
tracking/ subfolder for time-series entries, and a routines/ subfolder for reference docs).
Expanded stewardships accept tracking notes via cdno track. See
Stewardships and tracking.
question — important questions, kept visible
A question note has a domain (research or life) and a status (active, parked, answered,
retired). Questions anchor portfolios and projects (via the core_question link). List the active
ones any time with cdno questions.
commitment — dated promises
A standalone promise with a hard due: date and a context. It moves to
commitments/_done/<year>/ when it ends — stamped completed if it was kept, dropped if it was
cancelled or overtaken. Both are terminal; the frontmatter says which. Standalone commitments are one of four sources feeding the aggregated
commitments view.
These twelve are a closed set with built-in behaviour. For an entity they don’t cover (people,
books, clients), you can declare a schema-only custom note type
in config.toml — a folder, field rules, and a template, with no recompile (see the worked
Tracking people recipe).
One custom type comes declared for you: cdno init writes a concept type (concepts/,
mutable, refined in place) into a new vault’s config, for understanding you will reuse — a theorem,
a technique, a procedure. It is an ordinary custom type you may delete. See
The concept library.
For the exact frontmatter fields of each type, see Frontmatter fields. Next: The concept library.
Which notes can be renamed
cdno note rename takes:
| Type | Renamable? |
|---|---|
project | Yes. |
action notes | Active ones only; an archived action note (actions/_done/) is refused. |
question | A portfolio with the same slug moves with it, as a pair. |
portfolio | Directly, and its folder moves with it. A portfolio that shares a question’s slug is renamed through the question instead. |
| custom types | Any type declared in [note_types.<type>], as a flat note; a custom note kept as a folder (<dir>/<slug>/_index.md) is refused. |
Not renamable: daily, weekly and monthly (the journal is addressed by date), archived action
notes, commitment, stewardship, evidence, tracking, inbox items, folder notes (other than a portfolio’s) and a note with no type. They are refused with
note_not_renamable, and nothing is written. A rename corrects references, which is the one
exception to the append-only rule; see the method page.
The concept library
Some of what you work out is neither evidence nor work. What the Woodbury identity buys you when you invert a low-rank update. How to do a rolling restart of a StatefulSet without losing quorum. It answers no open question, so it is not evidence. It has no deliverable and never completes, so it is not a project or an action. It belongs to no perpetual responsibility, so it is not a stewardship routine. Left in the daily log, it is findable but scattered: three partial versions across three days, and the one you find first is not necessarily the best.
The cost of that is re-derivation — working the same thing out again because the last time is
buried, or because the half-versions disagree. A concept note is the fix: the single, current,
best account of one piece of understanding you expect to reuse. Concept notes together form a
library of concepts in a flat concepts/ folder, organised by tags and links rather than by
folders.
A concept note is refined in place as your understanding improves, and each refinement leaves a one-line trace in the daily log. It has no lifecycle, no staleness, no review obligation and no cap.
A custom type, declared for you
concept is not a thirteenth built-in note type. It is an ordinary
custom note type that cdno init declares in a new vault’s
.cuaderno/config.toml, with its template installed as .cuaderno/templates/concept.md:
# A declared custom type: the concept library (RFC 0002). Delete this block (and
# any notes under concepts/) if you do not want one; nothing else depends on it.
[note_types.concept]
folder = "concepts"
required = ["created"]
optional = ["tags", "origin"]
template = "concept.md"
If you do not want a library, delete the block. A vault created before the type existed gets the declaration, the template and the folder with one command, which writes only what is absent and never changes a declaration you already have (see Installing a bundled note type):
cdno config note-type install --name concept
A concept note looks like this:
---
type: concept
created: 2026-09-25
tags: [linear-algebra, optimisation]
origin: "[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]"
---
# Woodbury identity
## Statement
…
## Why it matters
…
## See also
- [[concepts/sherman-morrison]]
The title is the H1. tags is your subject vocabulary: open, indexed, and needing no
configuration. origin, written only when you supply it at creation, holds wikilinks to the
daily-note entries the concept came from. The body is free-form: Statement / Why it matters / See
also is the template’s default, and a procedure or a definition simply uses its own headings.
One concept per note. If a note needs two headings that could each be cited on their own, it is
usually two notes, linked. A single derivation that does not split can stay one note, with its
sections cited by heading link ([[concepts/woodbury-identity#Statement]]).
The filing test
Before you keep something, ask what is it?
- A dated observation, result or source that bears on an open question → evidence in a portfolio.
- The answer to a question you weighed evidence for → the question note, with
status: answered. Its## Current Thinkingmay be promoted into a concept that the question links to; the question keeps the record of how it was answered. - A prescribed practice belonging to one stewardship (a workout plan, a care routine) → a stewardship routine.
- Understanding you will reuse, independent of any deliverable — a theorem, a definition, a technique, a procedure, an idea → a concept note.
- Not yet settled → an entry in the day’s
## Notes, until you promote it.
And the things that are none of these stay where they already go: something to do is an action on a project map; finite work with a deliverable is a project.
A benchmark result is evidence. What the benchmark taught you about the technique is a concept. The command that ran it belongs in the evidence note — and may also become a concept, if you will run it again.
The provenance rule
Evidence never depends on the current text of a concept note. An evidence note records what was
done and observed inline — the exact invocation, the version, the parameters — and may link to a
concept only as see also. A concept is never the origin: of an evidence note. Concepts describe
your understanding now; evidence records what happened then. That separation is what lets
you refine a concept freely without rewriting the past.
Substance in the day, pointers in the log
## Logs is the day’s sequence: one line per event. Substance — a derivation, a worked
procedure, a page of reasoning — does not belong there, because a block of text turns a sequence
into a wall. It goes in the daily note’s ## Notes section instead, one ### <heading> entry
per piece, while the log gets a one-line pointer to it:
## Notes
### Woodbury identity
(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1.
Cheap when A^-1 is already known and the update is low-rank: today's use was the
k=3 refit on [[surrogate-model]]. #concept
## Logs
- **14:32**: noted [[journal/2026/daily/2026-09-02#Woodbury identity]] ([[surrogate-model]])
cdno log note (or the note_to_daily MCP tool) writes
both halves in one go: the entry under ## Notes, which it creates just above ## Logs if the day
does not have one yet, and the noted [[…#Heading]] pointer line in ## Logs, followed by the
entry’s own wikilinks in parentheses. So the day’s sequence shows that a derivation happened and
what it touched, while the derivation itself stays out of the way. Like ## Logs, ## Notes is
append-only.
Each entry’s heading is its address, so the tool holds it to a few rules: unique within the day,
never a daily section name (Standup, Intention, Agenda, Meeting, Notes, Logs), no [,
], |, # or inline markup, and not starting with ^. Headings inside the entry must be level 3
or deeper. If you write an entry by hand, follow the same convention.
Mark candidates. End an entry you might reuse beyond today with the tag #concept on its last
line. That is how you — or an assistant — find promotion candidates later.
## Notes is not read by the project, weekly or monthly context: those read ## Logs. An entry
about a project reaches that project through the pointer line’s wikilinks and the entry’s own links
(which become backlinks once the index is rebuilt; see Promotion), so a review sees
that you worked something out, not the working itself.
Promotion
Write a concept note straight away when you already know you will reuse the thing. When you are not sure, note it in the day and promote it the second time it comes up.
Promotion is simply creating a concept note with origin pointing at the entries it came from.
There is no separate promote operation, no counter, and no promoted to line:
cdno note create concept --title "Woodbury identity" --body-file woodbury.md \
--origin "[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]"
Creation is logged for you, to today’s daily note, as concept created [[concepts/<slug>]] — <title>,
and origin records where the concept came from. Over MCP the same step is create_custom_note
with body and origin.
The ## Notes entries stay as they were — the day’s record is append-only — and their daily notes
have a backlink from the concept. Every write through cdno indexes the links it writes, so links
from cdno log note, cdno note create or cdno note revise are in the backlinks at once.
Search before you create. A second note on a subject the library already covers is the
duplicate this whole method exists to avoid. cdno search <word> --type concept first; if the
concept exists, refine it rather than minting another. (Nothing refuses a second note with the same
title: it is created as <slug>-2.)
An assistant connected over MCP follows the same habit. Its tool descriptions tell it to search the
library before drafting an explanation, and to offer promotion when it finds #concept entries on
the same subject on two or more dates.
Finding candidates, and the current limitation
Search matches words, not tags. Searching concept across daily notes finds the entries tagged
#concept, but also — by stemming — any day that merely created, revised or linked a concept note,
since those log lines contain concepts/. The snippet rarely shows the entry’s subject either, so
open each hit and look for ## Notes entries whose last line carries #concept:
cdno search concept --type daily
A search that filters by tag is planned but not shipped.
Refinement
A concept note is mutable in place. A better explanation replaces the worse one; a correction replaces the error. The note is always the current best account — correct it rather than annotate it.
Refine through the tool, so the change leaves a trace:
cdno note revise woodbury-identity --section "Why it matters" --content-file why.md \
--reason "added the cost comparison"
Each revision that changes the text writes one line to today’s daily log:
- **14:32**: revised [[concepts/woodbury-identity#Why it matters]] — added the cost comparison
- The reason is required. It is the part of the history worth keeping, so say briefly why.
- The
#Sectionanchor is present when you revised one section, and absent for a whole-body rewrite. - There is no
was:/now:block, unlike a project’s state change. The log records when and why; git records what. - Text identical to what is already there writes and logs nothing.
You can replace the whole body (--body-file, or your editor when run interactively with no file)
or upsert one section (--section with --content-file). A section that exists is replaced
together with its sub-sections; one that does not is appended as ## <section>. There is no append
operation for a concept — refinement means rewriting what is there.
The hash guard. cdno note revise reads the note first and remembers a hash of what it read.
If the file changes on disk before the revision is written — say you saved it in your editor while
the revise editor was open — the revision is refused rather than overwriting that change: read the
note again and redo it. Over MCP, read_note returns that content_hash and a whole-body
revise_note must pass it back as expected_hash.
Edits you make directly in an editor are legitimate — the Markdown is the source of truth — but they
leave no log line. That is a known limit of the history, not something cdno lint polices.
Only mutable custom types can be revised this way. Built-in types (a project’s sections belong to
its own commands) and custom types declared append_only = true are refused.
No staleness
Understanding does not go stale. A theorem is as true after two untouched years as on the day you
wrote it down, and an explanation you have not changed is one you were happy with. So concept notes
have no verified date, no age signal, no staleness lint and no review chore. They never appear
in cdno orient, the weekly or monthly context, or any review list.
The history lives in the daily log instead: every creation and revision is a dated line there, so “how has my understanding of this evolved?” is a search, not a field.
Usefulness is a question you can ask, not a metric that is kept: the note’s backlinks (which notes
link to it) are one read_note away over MCP, and whether it has been mentioned in the log is a
search. An unreferenced concept costs nothing and is left alone. A procedure that depends on a
version can say “last run on 2026-09 against v1.4” in its own body if that matters; there is no
field for it.
Structure by links, not folders
concepts/ is flat. A concept usually belongs to several subjects at once — a theorem is both
linear algebra and optimisation — so a folder is the wrong axis. Tags carry membership, and
wikilinks carry relationships.
Hub notes carry navigation. When a cluster is real, write a concept note that is mostly links —
concepts/linear-algebra.md listing the theorems, the notation sheet, the techniques. It is an
ordinary concept note, written by hand when you need it, never a folder decided in advance. By
convention its slug is the tag it maps. It plays the part a portfolio’s _index.md plays for
evidence.
If the folder grows past a few hundred files and you browse it on disk, one coarse subfolder level
(concepts/maths/) is tolerated. The tool is blind to it: it never asks for a subfolder, and
[[concepts/<slug>]] still resolves after you move a file by hand.
Summary
| You want to… | CLI | MCP |
|---|---|---|
| Write substance into the day | cdno log note | note_to_daily |
| Find an existing concept | cdno search <word> --type concept | search_notes with note_type: concept |
| Read a note whole | cdno open concept:<slug> | read_note |
| Create or promote a concept | cdno note create concept --origin … | create_custom_note with origin |
| Refine a concept | cdno note revise | revise_note |
| List the library | cdno note list concept | — |
Walk one promotion end to end in Building a concept library.
Next: Vault structure.
Vault structure
A vault is a directory tree of Markdown files plus a .cuaderno/ config folder. cdno init creates
the whole layout; here’s what each part holds.
vault/
├── journal/
│ └── 2026/ # partitioned by (ISO) year
│ ├── daily/
│ │ └── 2026-04-25.md # type: daily (append-only)
│ └── weekly/
│ └── 2026-W17.md # type: weekly (append-only)
│
├── projects/
│ ├── surrogate-model.md # type: project (mutable)
│ ├── _parked/ # parked projects: complete maps that don't count
│ │ └── bayesian-opt.md
│ └── _done/
│ └── 2025/ # closed projects: completed or dropped
│ └── thesis-chapter-2.md
│
├── actions/
│ ├── characterise-sampler.md # type: action (manifest form)
│ └── _done/
│ └── 2026/ # completed actions, partitioned by year
│ └── run-ablation.md
│
├── portfolios/
│ └── sparse-vs-dense-ood/
│ ├── _index.md # type: portfolio
│ ├── 2026-03-15-chen-2025.md # type: evidence (append-only)
│ └── 2026-04-01-ablation-b.md
│
├── stewardships/
│ ├── finances.md # type: stewardship (flat)
│ └── health/ # expanded variant
│ ├── _index.md # type: stewardship
│ ├── tracking/ # type: tracking entries (append-only)
│ │ └── 2026-04-06-gym.md
│ └── routines/ # reference docs, not logs
│ └── upper-body-a.md
│
├── commitments/
│ ├── pay-rent.md # type: commitment
│ └── _done/
│ └── 2026/ # fulfilled commitments
│
├── questions/
│ ├── research/
│ │ └── surrogate-cost.md # type: question (domain: research)
│ └── life/
│ └── apartment-as-home.md # type: question (domain: life)
│
├── concepts/ # the concept library (a custom type `cdno init` declares)
│ ├── woodbury-identity.md # type: concept (mutable, refined in place)
│ └── linear-algebra.md # a hub note: mostly links
│
├── inbox/ # raw captures awaiting triage
│
└── .cuaderno/
├── config.toml # vault configuration
├── index.db # SQLite index cache (auto-created, rebuildable)
└── templates/ # note templates (override the built-ins)
Conventions worth knowing
_parked/(projects) and_done/(projects, actions, commitments) prefix folders hold inactive or finished notes. The underscore keeps them sorted out of the way and signals “not the active set.”_done/is partitioned by year so the active folders stay scannable.projects/_parked/holds complete maps that are set aside: they take every plan write (actions, state, milestones, waiting-on), keep their hard dates in the commitments register, and do not count toward the cap. You read them at the monthly review. A handed-off project (held_by:in its frontmatter) stays in whichever folder it was in; only the name changes. See Business rules._index.mdis the identity note of a folder — theportfolionote inside a portfolio folder, thestewardshipnote inside an expanded stewardship folder.tracking/inside a stewardship holds time-series entries;routines/holds prescriptive reference documents (a workout plan, a checklist) — those are not logs.concepts/is flat by design: tags and links, including hand-written hub notes, give it structure. See The concept library.- Stewardships have two shapes: a flat
stewardships/<slug>.md, or an expandedstewardships/<slug>/folder. Only expanded ones can hold tracking entries.
.cuaderno/
config.toml— vault settings: the method version the vault follows ([vault] method_version), the project cap, the holder deny-list ([projects] holder_sentinels), ignore globs, template behaviour, schema extensions, variables. See Configuration.index.db— a SQLite cache of the vault, used for fast search, linting, and link queries. It is rebuilt automatically when it’s stale (see Business rules); deleting it is safe. The Markdown files are always the source of truth.templates/— the note templatescdnofills when scaffolding a note. The built-ins are written here atinitso you can edit them; pure variable substitution, no logic.
Next: Business rules.
Business rules
Cuaderno enforces a small set of rules that keep the method honest. Knowing them explains why some commands behave the way they do.
Markdown is the source of truth
The files on disk are canonical. The SQLite index in .cuaderno/index.db is a cache — it makes
search, linting, and link lookups fast, but it can always be rebuilt from the Markdown. A stale
index is recoverable; a stale file would be data loss, so the tool never lets the cache override
the files. You can delete index.db at any time, or rebuild it explicitly with
cdno reindex.
Startup reconciliation
On every CLI invocation, MCP session, and app launch, Cuaderno reconciles the index against the filesystem — comparing modification times and content hashes — and quietly repairs anything stale before doing your command. This is why edits you make to notes by hand (in your editor, via sync) are picked up without a manual reindex.
The five-project cap
At most five projects count at once. A project counts when it is active and names no one
else as its holder: parked projects don’t count, closed ones don’t, and neither does an active
project that someone else is moving (below). Creating a project at the cap
creates it parked; the cap is checked whenever a project that did not count begins to: on
activation, so activating a sixth stops you — you must park one
first — and when you take an active project back from its holder. Parked projects live in
projects/_parked/. Closed ones are not open at all: a project that is
finished is completed, and one that is not
going to happen is dropped, both into
projects/_done/<year>/. Closing never needs a slot, and a parked project can be closed directly. The limit is configurable via config.toml
(see Configuration); five is the default because it’s the point past which
“active” stops meaning anything.
Parked projects are complete maps
Parking changes two things only: the project does not count, and you read it at the monthly review rather than daily. A parked map takes every plan write an active one does: next actions, Current State, milestones, waiting-on items, the core question and a holder, from the CLI and over MCP alike, so you never activate a project just to write on it. Its hard milestones and action due dates stay in the commitments register.
Only starting, switching or resuming work, and parking need an active project
(cdno action start, switch, resume, and
cdno project park): a project you start work on is
a project you are moving, so it counts. Activate it first, parking another if the cap is full.
Handed-off projects
An open project may name who else is moving it in a held_by: line at the top of its map:
held_by: "the harbour office". cdno project hand-off
writes it and take-back clears it, each
logging a holder on [[<slug>]] entry in today’s daily note. An active project that names a holder
stays where it is, keeps its actions, its log and its dates in the register, and does not count
toward the cap: the cap counts your attention, not someone else’s.
Three guards keep it honest. If you cannot name who has it, it is yours. A handed-off map says what you are waiting for in its Waiting On. And handing off is never the answer to a full cap: name a holder only when someone else really has the project. To make room, park, complete or drop one.
The deny-list. A held_by: value that names you or nobody (me, tbd, someone, ?, …)
reads as no holder, so the map stays yours and counts, and the hand-off verb refuses it. Those
values are a config key, [projects] holder_sentinels,
with a default list you can replace, or set to [] to accept any name. Its stated limit: the tool
cannot know your own name, so held_by: "<your name>" is read as someone else unless you add your
name to the list. A value that is not a plain name (an unquoted wikilink, a number, a date) also
reads as no holder; cdno lint reports it so you can quote it.
The method version
The vault records which version of the Research Logbook Method it follows, as
[vault] method_version in .cuaderno/config.toml. That is the method’s version, not cdno’s:
cdno init writes the version this cdno implements, and only
cdno migrate changes it. A vault with no key is read as RLM 1.0. A
cdno refuses to write to a vault on another major version (an older one: migrate it; a newer
one: upgrade cdno), while reads keep working; a minor difference only prints a notice.
Append-only notes
daily, weekly, evidence, and tracking notes are append-only — Cuaderno only ever grows
them, never overwrites, with one exception: when a note is renamed, references to it are corrected to the new name, even here. They are the historical record. (Projects are mutable on purpose; see
next.)
Project state history is preserved
A project’s Current State is the one piece of freely-mutable prose in the vault. To keep history
intact, every time you update it (via cdno project state or the MCP
update_project_state tool) the previous state is auto-logged to today’s daily note before the
new text overwrites it. You get a clean current view and a full audit trail in the journal. A
hand-off or take-back logs the same way, as holder on [[<slug>]] with was: and now: lines.
Commitments are aggregated, not stored in one place
The commitments view is computed from four sources, so a promise is counted wherever it naturally lives:
- Project milestones marked with a hard deadline (
--hard), of an open (active or parked) project. - Stewardship periodic commitments (the recurring lines on a stewardship dashboard).
- Standalone commitment notes in
commitments/. - Action notes carrying a self-imposed
due:that isn’t pinned to a milestone, of an open (active or parked) project.
A project’s own dates (sources 1 and 4) count while the project is open, active or parked: a hard date on a parked map is still a promise. Completing or dropping a project takes its milestones and action due dates out of the view until it is activated again. A standalone commitment that names a parked or closed project keeps surfacing, since it is a promise to someone else.
cdno commitments merges and sorts all four by date, with overdue
items flagged.
Atomicity and its limits
Each write is captured as a transaction (file writes + index updates) that commits as a batch and
rolls back on failure — while the process is alive. Startup reconciliation catches index staleness
afterward, but a crash midway through a rare multi-file operation can still leave partial state on
disk. In practice this is vanishingly rare; the takeaway is simply that the Markdown is authoritative
and reindex + lint will surface
anything odd.
Next: Contexts and energy.
Contexts and energy
Two small enumerations show up across many commands. Both are fixed vocabularies (not free text), so they stay consistent and filterable.
Contexts — the life domain
A context classifies which part of life a project, stewardship, or commitment belongs to. The set is fixed:
| Context | Typical use |
|---|---|
work | Your main job |
side-project | Personal projects outside work |
university | Studies, coursework, a degree |
family | Family responsibilities |
household | Running a home |
legal | Paperwork, contracts, official matters |
personal | Health, growth, anything else personal |
You set a context when you create a project (--context), stewardship (--context), or commitment
(--context). It groups and colours items in views and lets the system reason about balance across
your life, not just your work.
Contexts are a compile-time set, not configurable — keeping the vocabulary small and shared is the point. (Stewardships accept the same set.)
Energy — the effort a thing needs
An energy level tags how much focus an action demands, so the morning suggestion can match the work to how you actually feel:
| Energy | Meaning |
|---|---|
deep | Heavy, uninterrupted focus (real thinking, hard implementation) |
medium | Moderate focus (routine progress, review) |
light | Low focus (admin, quick wins, tidying) |
You tag an action with --energy when you add it. Then:
# "I have a clear morning" — bias the suggestion toward deep work:
cdno orient --energy deep
# "I'm fried" — surface something light instead:
cdno orient --energy light
cdno orient uses the energy bias to pick which next action to
suggest as your starting point. Matching the task to your state — rather than forcing the hardest
thing first — is part of what makes the daily loop sustainable.
Focus
Energy says what kind of work to pick; focus says what you are on right now. It is a single slot, and it is deliberate at both ends: you put something in it, and you take it out.
| To… | Run | Log line |
|---|---|---|
| put an action in the slot | action start | started [[slug]] — text |
| finish it | action complete | action done on … |
| abandon it | action drop | action dropped on … |
| set it aside, to come back | action pause | action paused on …, with next: and reason: |
| move to something else | action switch | a paused line then a started line |
| pick it up again | action resume | resumed [[slug]] — text |
cdno now reads it back. Nothing is stored beside the daily log: the
focus is replayed from it each time, so there is nothing to keep in sync and a line you write by
hand counts as long as it has the writers’ shape.
One slot. Starting something while something else is in focus is refused, naming the way forward
(switch, or pause first). You can always move on; you just say so, which makes the move a line in
the log instead of a silent change of subject. The same holds on reading: a newer start displaces an
older one for good, and finishing the newer one does not bring the older back.
The window. A start left open at midnight is still the focus in the morning, with its day named,
and expires after that. The window is carry_over_days, default
1; set it to 0 to read today only. resume re-anchors a carried focus to today, so work you keep
going on does not expire, and work you stop lets it lapse.
Pause and resume. pause is the honest way to stop without finishing: the action stays open on
the map, and an optional --next records where to pick up. Pauses are remembered for
paused_lookback_days, default 14, which covers a weekend. A
paused action stops being offered once something later started, resumed, finished, dropped or
promoted it. Promoting a bullet while it is in focus is fine: the focus follows it to the new note.
With an assistant. Over MCP the focus rides along on
the reads and on the results of eighteen write tools, so an agent knows what you are on without asking. The server
never refuses a write for being outside the focus. The agent’s job is to notice a change of subject
and say it in one sentence, offering to capture the thought and stay, to do it now as an aside, or to move over if you say
so, and to give you a cue to get back (next: is what it reads out). It raises a given topic at
most once per focus, never asks you why, and never pauses or resumes without your yes.
The during: tag. While a focus is open, a log line written by cdno log, an inbox capture, and
a ## Notes entry (on its noted pointer line in ## Logs) are tagged with its project:
during: [[surrogate-model]] under the log line,
captured_during: surrogate-model in an inbox item. The tag is what lets a later review see where
captures came from, and it survives triage because the discard line copies it.
Next: Configuration.
Configuration
Vault behaviour is configured in .cuaderno/config.toml, written for you by cdno init. The
defaults are sensible — you can run for a long time without touching it. This page explains what is
configurable and why; the Configuration reference lists every key.
What you can configure
- The method version.
[vault] method_versionrecords which version of the Research Logbook Method the vault follows: the method’s version, not cdno’s.cdno initwrites it; you change it only withcdno migrate, never by hand. A vault without it is read as RLM 1.0. - The active-project cap. Change the default of five via
[vault] max_active_projects. It counts the projects that are active and name no one else as their holder. - The holder deny-list.
[projects] holder_sentinelslists theheld_by:values that name you or nobody (me,tbd,someone, …), so a map carrying one stays yours and counts. Replace the list to add your own name or words in your language, or set[]to accept any name. The tool cannot know your own name: add it if you want it read as yours. - Ignore globs. Patterns for files the index should skip —
CLAUDE.md,README.md, scratch notes — so they don’t appear in search, lint, or link checks. Patterns are additive (no negation), matched against vault-relative paths, and never delete anything on disk — they only scope what the index considers. - Templates. Override any built-in note template by adding a file under
.cuaderno/templates/. - Custom note types. Declare your own types under
[note_types.<name>].cdno initdeclares the bundledconcepttype for you; a vault created before it existed gets it withcdno config note-type install --name concept(see Custom note types). - Schema extensions. Add vault-specific required frontmatter fields per note type (e.g. require
collaboratorson every project), enforced bycdno lint.
The hands-on walkthrough is Customising templates and frontmatter.
Templates
When cdno scaffolds a note, it fills a template. Templates are pure variable substitution — no
conditionals, no logic. cdno init writes two starter templates (.cuaderno/templates/daily.md, and
concept.md for the concept type it declares); every other built-in type uses its built-in
default until you add a file for it. cdno picks the most
specific template that exists:
- a custom variant template (for tracking, e.g.
tracking-gym.md), then - a custom type template (e.g.
project.md), then - the built-in variant default, then
- the built-in type default.
Because templates also define the canonical order of frontmatter keys,
cdno normalise uses them to reorder hand-authored or migrated
notes into a consistent shape.
Variables
Templates use {{placeholder}} markers that cdno fills from the values each note’s creation
command supplies — {{title}}, {{context}}, {{created}}, and so on. The exact set available per
note type, and how to use them in a custom template, is covered in
Customising templates and frontmatter. An unknown
placeholder is left verbatim, so use only the ones a type provides.
Custom templates can also reference static vault variables you set under [variables] in
config.toml (e.g. {{author}}); these resolve on every note type, with per-type values taking
precedence over a config variable of the same name.
For values that change per note, prompted variables under [variables.prompt] are gathered at
creation — from a --var name=value flag, an interactive prompt, or (failing both) a clear error.
The tutorial covers them in full.
Example
[vault]
name = "My Research Vault"
method_version = "1.2" # written by `cdno init`, changed only by `cdno migrate`
max_active_projects = 5 # the active-project cap
# Skip these from the index entirely (search/lint/links). Never deletes files.
ignore = ["CLAUDE.md", "README.md"]
# Require an extra field on every project note — enforced by `cdno lint`:
[schemas.project]
extra_required = ["collaborators"]
# Names that mean "me" or "nobody" in a project's `held_by:` (replaces the default list):
[projects]
holder_sentinels = ["me", "tbd", "someone", "A. Researcher"]
# Static template variables — resolve in any custom template (e.g. {{author}}):
[variables]
author = "A. Researcher"
# Prompted variables — gathered at note creation (--var, prompt, or error):
[variables.prompt]
collaborators = "Who are the collaborators?"
For the complete key-by-key reference, see Configuration reference.
That’s the concepts tour — next, put it to work in the Tutorials.
The daily loop
The core rhythm of Cuaderno is a daily loop: orient → act → log → close. None of it is mandatory or guilt-inducing; it’s a habit that keeps the vault current with almost no overhead.
Morning: orient
Start the day by asking the tool what deserves attention:
cdno orient --energy deep
orient shows commitments due soon, your active projects with their
current state and top next action, and a single suggested starting point — biased toward the
energy you tell it you have. Drop --energy to get a neutral
suggestion; pass --energy light on a low day.
Want just the project snapshot without commitments? Use cdno status.
If you left something open yesterday, cdno now still shows it this morning, with the day named. Pick
it up with cdno action resume, or set it aside with cdno action pause --next "…" and choose
something else. orient itself does not list the focus or your paused work; cdno now does.
Through the day: act and log
As you work, two verbs carry most of the weight.
Log what happens — append a timestamped line to today’s journal:
cdno log "scaled the mesh to 2M cells; 4x runtime, still stable"
Capture stray thoughts without breaking flow — they land in the inbox for later triage:
cdno capture "does Chen 2025 use the same preconditioner?"
Make progress on projects as you go:
# Add the next action you just thought of:
cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium
# Record where a project now stands (auto-logs the previous state to today's journal):
cdno project state --slug surrogate-model --text "Mesh scaling works; assembly is the bottleneck"
# File a useful result into the right portfolio:
cdno file --portfolio sparse-vs-dense-ood --source "ablation run B" --origin projects/surrogate-model
# Say what you are starting, so `cdno now` can answer for you later:
cdno action start --project surrogate-model --query "assembly step"
# Tick off a finished action (substring match on the bullet):
cdno action complete --project surrogate-model --query "feature set B"
When you lose the thread, ask what you were doing rather than guessing:
$ cdno now
On surrogate-model — Profile the assembly step (medium), since 09:30 (1h 30m).
There is no state behind that — it replays today’s journal and yesterday’s (the
[focus] carry_over_days window in .cuaderno/config.toml), so a start made here,
or by an agent over MCP all count equally, and completing, dropping or pausing the action clears it.
With nothing open it says so, and shows the last thing you paused and the note you left yourself. See
cdno now for the full shape, including what a start typed by hand has to
look like.
When the day changes direction, say so rather than stacking one thing on another. Starting a second action while one is open is refused; the way through is a verb:
# Stopping for now, with a note to your future self:
cdno action pause --next 'pick up at "Prior approaches"'
# Moving to something else right now (a pause and a start together):
cdno action switch --project other-project --query "CI"
# Coming back; prints the note you left:
cdno action resume
Each one is a line in the journal, so the record shows when you moved and, if you said, why. When you capture a thought while something is in focus, the inbox item remembers which project you were on. See Focus.
Evening: close
A light wind-down — note anything for your stewardships and reflect:
# Log a tracked activity under a stewardship:
cdno track gym --stewardship health --content "Upper body; good energy"
# A closing reflection is just another log line:
cdno log "good focus day; pick up assembly profiling tomorrow"
That’s it. The journal now holds a faithful record of the day, your projects reflect reality, and
tomorrow’s orient will pick up where you left off. Anything still in focus at midnight is carried
into tomorrow, and cdno action pause is the way to put it down on purpose.
Letting Claude run the loop
Each step has an MCP equivalent (get_orientation, append_to_log, add_action,
update_project_state, …), so an assistant can drive the same loop conversationally. See
Connect to Claude and the example
skills.
Next: Managing projects.
Managing projects
A project is a lightweight map of a piece of active work: a current state, next actions,
milestones, and things you’re waiting on. You keep at most five active
at once. All the verbs live under cdno project (plus
cdno action for the next-action list).
Create one
cdno project create --title "Surrogate model" --context work
# -> projects/surrogate-model.md (slug derived from the title)
--context is the life domain. Optionally link the project’s
core question with --question questions/research/surrogate-cost — and if you skip it, or the
question changes later, cdno project core-question --slug surrogate-model --question <target>
sets it afterwards (--clear detaches). If you’re already at five active projects, the new one is
created parked — activate it once you free a slot.
See where things stand
cdno project list # active projects + a state snippet
cdno project show surrogate-model # one project in detail
cdno status # all active projects + their top action
Add --json to any of these for structured output (see JSON output).
Update the current state
The Current State is the project’s one mutable paragraph — “where is this right now?”. Updating it auto-logs the previous state to today’s journal first, so you never lose the trail:
cdno project state --slug surrogate-model \
--text "Mesh scaling works to 2M cells; assembly is now the bottleneck"
Next actions
Actions are the things to do next. By default they’re inline bullets on the project:
cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium
cdno action list --project surrogate-model
cdno action complete --project surrogate-model --query "profile the assembly"
See Actions for the inline-vs-manifest distinction and promotion.
Milestones
Milestones are markers of progress. Mark one --hard to make it a real deadline that shows up
in the aggregated commitments view:
cdno project milestone add --slug surrogate-model --title "Submit to ICML" --date 2026-01-22 --hard
cdno project milestone done --slug surrogate-model --query "submit to icml"
--date is optional. When a milestone is gated by a condition rather than a date, leave it off
rather than inventing an estimate — a made-up date reads back later like a commitment somebody
made:
cdno project milestone add --slug surrogate-model --title "All Round-1 replies received"
An undated milestone records target: TBD, stays out of the commitments view, and completes
exactly like a dated one. --hard needs a real date.
Waiting-on
Track external blockers so they’re visible instead of forgotten:
cdno project waiting add --slug surrogate-model --description "Cluster quota increase from IT"
cdno project waiting resolve --slug surrogate-model --query "cluster quota"
Park and re-activate
Parking is first-class and reversible — it’s how you respect the five-project cap without deleting anything:
cdno project park --slug surrogate-model # -> projects/_parked/, frees a slot
cdno project activate --slug surrogate-model # bring it back (must be under the cap)
A parked map is still a complete map. Parking changes two things only: the project stops counting, and you read it at the monthly review instead of every day. You can keep writing on it exactly as on an active one, without activating it first:
cdno action add --project bayesian-opt --title "Re-read the acquisition-function survey" --energy light
cdno project state --slug bayesian-opt --text "Shelved until the ICML submission is in"
cdno project milestone add --slug bayesian-opt --title "Decide whether to restart" --date 2026-07-01
Its hard milestones and dated actions stay in cdno commitments: a deadline on a
parked map is still a promise. What a parked map refuses is work: cdno action start (and
switch and resume) needs an active project, because a project you are working on is one you are
moving. If you find yourself working on a parked map, activate it, parking another if the cap is
full.
Hand a project off
Some projects stay open while someone else is moving them: the survey is with the harbour office, the draft is with your co-author. Name the holder, and the map says so:
cdno project hand-off --slug hull-survey --to "the harbour office"
# Handed hull-survey to the harbour office (projects/hull-survey.md)
The map gains held_by: "the harbour office", today’s daily note gets an entry:
- **09:10**: holder on [[hull-survey]]
was: (mine)
now: the harbour office
and the project, if active, no longer counts toward the cap: cdno project list shows it with a
held by: the harbour office · does not count toward the cap line. It stays where it is, keeps its
actions, log and deadlines, and is still read at the weekly review, where the question is not “did
it move?” but “what came back, and what is waiting on my decision?”. Say what you are waiting for:
cdno project waiting add --slug hull-survey --description "Inspection report from the harbour office"
When the work comes back to you, take it back:
cdno project take-back --slug hull-survey
# Took hull-survey back (projects/hull-survey.md)
That logs holder on [[hull-survey]] with now: (mine), and an active project counts again, so at
the cap the take-back is refused with the projects that count. A parked project stays parked; taking
it back only clears the name.
Three guards keep it honest:
- If you cannot name who has it, it is yours.
--to tbd,--to someoneor--to meis refused (the list is[projects] holder_sentinels). - A hand-off is not made mid-work. While an action on the project is in focus, pause it first.
- Handing off is never how you make room at the cap. Name a holder only when someone else really has the project. To free a slot, park, complete or drop one.
Close a project
A project ends one of two ways, and the log records which:
cdno project complete --slug surrogate-model # the work is done
cdno project drop --slug bayesian-opt --reason "superseded by the ICML work" # it is not happening
Both move the map to projects/_done/<year>/, stamp closed: with today’s date and free the slot;
both work on a parked project too. If anything is still open, the command stops and lists it:
surrogate-model has 2 open items:
- [ ] Draft methods section with ablation figures (medium)
- [ ] ICML paper submitted — hard: 2026-05-22
Complete or drop each of them (or add it to the project that now owns it), then run again.
Tick what was done, drop what is not happening (each with its own reason), and run complete
again. A drop can instead let the open items go with the project: it asks first, or takes
--drop-open in a script. Commitments you made to other people are never touched by either.
cdno project list --closed shows what has ended, newest first, and the MCP weekly and monthly
contexts (get_weekly_context, get_monthly_context) list the projects that closed in their window. A closed project can come back with
cdno project activate, or take the other outcome later if the first one was wrong.
Next: Research and evidence.
Research and evidence
This is the knowledge-building half of the method: name the questions that matter, open a portfolio for each, and file evidence into it as you go. Over months a portfolio becomes a dossier you can actually reason from.
Name a question
cdno question create --domain research --text "Does sparse attention beat dense out-of-distribution?"
# -> questions/research/does-sparse-attention-beat-dense-out-of-distribution.md
--domain is research or life. List the active ones any time:
cdno questions # grouped by domain
cdno questions --json | jq .
As a question’s life changes, transition it (each transition is logged to the journal):
cdno question park --slug does-sparse-attention-beat-dense-out-of-distribution
cdno question answer --slug does-sparse-attention-beat-dense-out-of-distribution
cdno question retire --slug does-sparse-attention-beat-dense-out-of-distribution
cdno question activate --slug does-sparse-attention-beat-dense-out-of-distribution
Open a portfolio for it
A portfolio is a folder that accumulates evidence about one question:
cdno portfolio create --question "Sparse vs dense attention OOD"
# -> portfolios/sparse-vs-dense-attention-ood/_index.md
# Optionally tie it to a project when you create it:
cdno portfolio create --question "Sparse vs dense attention OOD" --project projects/surrogate-model
Already have a portfolio and want to link it after the fact? Use the retrofit verb (one of
--question/--project, not both):
cdno portfolio link --portfolio sparse-vs-dense-attention-ood --project projects/surrogate-model
File evidence
Every useful artefact — a paper, an experiment result, a conversation — goes in as an evidence
note. --source is the citation/reference; --origin is a wikilink to whatever produced it:
cdno file --portfolio sparse-vs-dense-attention-ood \
--source "Chen et al. 2025, NeurIPS" \
--origin projects/surrogate-model \
--content "They report a 4x speedup at 95% accuracy on the OOD split."
Attaching a real file
To file a non-Markdown artefact (PDF, image, video), point --attach at it. Cuaderno copies it into
the portfolio and scaffolds a linked evidence stub beside it (here --content is the abstract). Add
--move to move instead of copy:
cdno file --portfolio sparse-vs-dense-attention-ood \
--source "Chen et al. 2025" \
--origin projects/surrogate-model \
--attach ~/Downloads/chen2025.pdf \
--content "Key result: 4x speedup at 95% accuracy."
Review what’s accumulated
cdno portfolio list # all portfolios + evidence counts + staleness
cdno portfolio show --portfolio sparse-vs-dense-attention-ood
cdno search "preconditioner" --portfolio sparse-vs-dense-attention-ood
portfolio list flags staleness — dossiers you haven’t fed in a while — which is a useful prompt
during your monthly scan. Periodically synthesise the findings into the portfolio’s _index.md.
Not everything worth keeping is evidence. What a result taught you about a technique — understanding you will reuse, independent of any question — is a concept, and it lives in its own library; see The concept library for the filing test.
Next: Building a concept library.
Building a concept library
This walkthrough follows one piece of understanding from a scribble in the day to a refined concept note: you work something out, note it, meet it again three weeks later, promote it, and then improve it. The method behind each step is in The concept library.
It assumes a vault created by cdno init, which declares the concept type. For an older vault,
first run cdno config note-type install --name concept.
The project surrogate-model stands in for whatever you are working on.
The input files below are scratch; write them outside the vault (for example under /tmp), or a
stray .md in the vault root is reported as unindexable. The commands here use /tmp.
1. Note the substance in the day
On 2 September you work out why a low-rank refit is cheap. It is not a one-line event, so it does
not go in ## Logs; write it to a file (or let the editor open, below) and log it as a note:
cat > /tmp/woodbury.md <<'EOF'
(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1.
Cheap when A^-1 is already known and the update is low-rank: today's use was the
k=3 refit on [[surrogate-model]]. #concept
EOF
cdno log note --heading "Woodbury identity" --body-file /tmp/woodbury.md
Noted journal/2026/daily/2026-09-02#Woodbury identity
The body ends with #concept because this looks reusable. Run interactively without the flags,
cdno log note asks for the heading and opens the body in your editor instead.
The daily note now holds the entry under ## Notes, and a pointer to it in ## Logs, with the
entry’s wikilinks copied onto the pointer line:
# Wednesday, 2 September 2026
## Notes
### Woodbury identity
(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1.
Cheap when A^-1 is already known and the update is low-rank: today's use was the
k=3 refit on [[surrogate-model]]. #concept
## Logs
- **14:32**: noted [[journal/2026/daily/2026-09-02#Woodbury identity]] ([[surrogate-model]])
## Notes did not exist yet, so it was created just above ## Logs. You did not have to decide
whether this deserves a note of its own — that decision can wait.
2. Meet it again
On 24 September the same identity comes up in a k=5 refit. Note it again, under a heading of its
own for that day. --date writes to a chosen day’s note (stamped at the current time); --json
shows everything the command returns:
printf 'Refit again with k=5 on [[surrogate-model]]: Woodbury keeps the update at O(k^3) rather than a full re-inverse.\n#concept\n' > /tmp/refit.md
cdno log note --heading "Low-rank refit" --body-file /tmp/refit.md --date 2026-09-24 --json
{
"log_line": "noted [[journal/2026/daily/2026-09-24#Low-rank refit]] ([[surrogate-model]])",
"message": "Noted journal/2026/daily/2026-09-24#Low-rank refit",
"path": "journal/2026/daily/2026-09-24.md",
"target": "journal/2026/daily/2026-09-24#Low-rank refit"
}
target is the entry’s address: cite it anywhere as [[<target>]].
A heading already used that day, or one that reuses a section name, is refused and nothing is written:
$ cdno log note --heading "Logs" --body-file /tmp/refit.md --date 2026-09-24
Error: heading `Logs` is not allowed in `## Notes`: it is the name of a daily section
3. Find the candidates
The subject has now come up on two dates — the moment to promote. Search the daily notes for the tag’s word:
cdno search concept --type daily
Search: concept
▎ 1. Thursday, 24 September 2026 daily
▎ journal/2026/daily/2026-09-24.md
▎ …a full re-inverse. #[concept] ## Logs - **14:40**: noted [[journal…
▎ 2. Wednesday, 2 September 2026 daily
▎ journal/2026/daily/2026-09-02.md
▎ …refit on [[surrogate-model]]. #[concept] ## Logs - **14:32**: noted [[journal…
Search matches the word concept, not the tag, so in a lived-in vault it also returns days
that only created, revised or linked a concept note. Open each hit (cdno open 2026-09-24) and read
the ## Notes entries that end in #concept to see which share a subject.
4. Check the library first
Before you create anything, make sure the library does not already hold this concept:
cdno search woodbury --type concept
Search: woodbury
(no matches)
Had there been a hit, you would refine that note (step 6) rather than create a second one.
5. Promote: create with origin
Write the concept’s opening in a file — without the # Title line, which the engine writes — and
create the note with origin pointing at both entries:
cat > /tmp/concept-body.md <<'EOF'
A low-rank correction to a matrix whose inverse you already hold can be inverted
without starting again.
EOF
cdno note create concept --title "Woodbury identity" --body-file /tmp/concept-body.md \
--origin "[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]"
Created concepts/woodbury-identity.md
---
type: concept
created: 2026-09-25
tags: []
origin: '[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]'
---
# Woodbury identity
A low-rank correction to a matrix whose inverse you already hold can be inverted
without starting again.
## Statement
## Why it matters
## See also
The body fills the template’s {{body}} slot, and the template adds its three sections after it.
origin is stored as a single quoted string. Its links are indexed with the write, so the daily
notes it names show the concept among their backlinks straight away.
Fill in tags in your editor (tags: [linear-algebra]). A --field tags=… value is written as a
plain string, not a list, so the tag list is easiest to edit by hand.
The creation is logged to the day you run it, and you do not log it again:
- **09:10**: concept created [[concepts/woodbury-identity]] — Woodbury identity
6. Refine it
Fill the empty sections one at a time. Each call names the section, the file holding its new text (without the heading), and a reason:
printf '(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1\n' > /tmp/statement.md
cdno note revise woodbury-identity --section Statement --content-file /tmp/statement.md \
--reason "stated the identity"
Revised concepts/woodbury-identity.md
With --json, the result says what was logged:
printf 'Refitting a rank-k update costs O(k^3) plus the products with A^-1, instead of a fresh O(n^3) inverse; the k=5 refit on [[surrogate-model]] is the worked case.\n' > /tmp/why.md
cdno note revise woodbury-identity --section "Why it matters" --content-file /tmp/why.md \
--reason "added the cost comparison" --json
{
"changed": true,
"log_line": "revised [[concepts/woodbury-identity#Why it matters]] — added the cost comparison",
"message": "Revised concepts/woodbury-identity.md",
"new_hash": "1d1d1cbac3cb10a7",
"path": "concepts/woodbury-identity.md",
"section_target": "concepts/woodbury-identity#Why it matters"
}
Running the same revision again changes nothing, so nothing is written or logged:
$ cdno note revise woodbury-identity --section Statement --content-file /tmp/statement.md --reason "again"
No change to concepts/woodbury-identity.md
The reason is not optional. In a script, leaving it out is an error; in a terminal, you are asked for it:
$ cdno note revise woodbury-identity --no-interactive --section "See also" --content-file /tmp/see-also.md
Error: missing required flag: --reason (provide it explicitly or run interactively in a TTY)
To rewrite the whole body instead, pass --body-file, or run cdno note revise woodbury-identity
in a terminal with no file and the current body opens in your editor. If the note changes on disk
while that editor is open, the revision is refused rather than overwriting the change.
Only custom types that are not append-only can be revised; a project, for one, is refused:
$ cdno note revise surrogate-model --body-file /tmp/why.md --reason "tidy"
Error: note 'projects/surrogate-model.md' cannot be revised: 'project' is a built-in note type; its sections are owned by its own commands
7. Read the trail
The day’s ## Logs now tells the concept’s story in three lines, and the two earlier days keep
their noted pointers:
## Logs
- **09:10**: concept created [[concepts/woodbury-identity]] — Woodbury identity
- **09:14**: revised [[concepts/woodbury-identity#Statement]] — stated the identity
- **09:21**: revised [[concepts/woodbury-identity#Why it matters]] — added the cost comparison
There is no was: / now: copy of the old text: the log records when and why, and if you keep
the vault in git, git records what. Tracing how your understanding of the identity evolved is a
search over the journal: cdno search woodbury --type daily.
With an assistant
Over MCP the same walk uses four tools, and the assistant’s tool descriptions carry the habits above:
note_to_dailywithheadingandbody— step 1 and 2.search_noteswithquery: "concept"andnote_type: "daily", thenread_noteon each hit — step 3.search_noteswithnote_type: "concept", thencreate_custom_notewithtype_name: "concept",title,bodyandorigin— steps 4 and 5.revise_notewithsection,contentand areasonit drafts from what it changed — step 6. A whole-bodyrevise_noteneeds thecontent_hashfromread_noteasexpected_hash.
See Write tools and Context-gathering tools for the parameters and refusal codes.
Next: Actions.
Actions
An action is the next concrete thing to do on a project. Cuaderno treats actions as cheap by
default: the normal form is a single bullet on the project map, not a note you have to create and
maintain. You only give an action its own note when it grows into real investigation. All verbs live
under cdno action.
Add a next action
cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium
This appends a checkbox bullet to the project’s next-actions list, tagged with its energy. That’s usually all an action ever is.
List open actions
cdno action list --project surrogate-model
Bullets that have been promoted to notes show their status (active / blocked / completed /
dropped) inline.
--json gives the structured list.
Promote an action to a manifest note
When an action becomes an investigation that spans days and produces evidence, promote it. This
rewrites the bullet as a wikilink and scaffolds an action note (with status, energy, criteria,
links). The match is a case-insensitive substring of the bullet text; energy is inherited:
cdno action promote --project surrogate-model --query "profile the assembly"
You can also create an action already promoted by passing --note to add:
cdno action add --project surrogate-model \
--title "Characterise sample efficiency across mesh sizes" \
--energy deep --note
Start an action
Starting is optional — nothing forces you to declare it — but it is what makes
cdno now able to answer “what am I in the middle of?”, which matters
most on the days you come back from an interruption and cannot remember:
cdno action start --project surrogate-model --query "assembly step"
This logs started [[surrogate-model]] — Profile the assembly step (medium) to today’s journal.
What gets written is the resolved bullet text, not your query, so the later completion logs
matching text and the focus clears by itself.
For work that is on no map yet — the fix you noticed, the errand in front of you — --unplanned
adds the bullet and starts it in one go:
cdno action start --project surrogate-model --unplanned \
--title "Chase the licence renewal" --energy light
That is deliberately a separate flag rather than a fallback when --query matches nothing. A
fallback would turn every typo into a new action, silently.
Promoting an action between starting it and closing it is fine: promotion rewrites the bullet,
and cdno now follows it to the new note, keeping the original start time.
Pause, switch and resume
Only one action is in focus at a time, so starting a second while the first is open is refused, with the command that does what you meant:
$ cdno action start --project surrogate-model --query "feature set B"
Error: Profile the assembly step is already in focus on surrogate-model (since 09:30).
To move on to this instead: cdno action switch --project surrogate-model --query 'feature set B'
Or complete or pause it first.
There are three ways to leave an action without finishing it, and they say different things:
# Stopping for now; leave yourself a note on where you were.
cdno action pause --next 'pick up at the profiler output' --reason "meeting"
# Moving to something else right now; pauses the open one and starts the new one together.
cdno action switch --project surrogate-model --query "feature set B" \
--next 'profiler output half read'
# Coming back, later today or after a night's sleep.
cdno action resume
resume prints the next: you left, and brings the action back into focus as of now. Unlike
drop, a pause does not close the bullet: it stays on the map and you can still complete it later.
switch is a pause immediately followed by a start, and shows in the log as exactly that. In a
terminal, pause and switch ask once where to pick up; press Enter to skip.
A start left open overnight is still your focus in the morning (cdno now says
since Saturday 14:05). cdno action resume makes it today’s; otherwise it lapses after a day. See
Focus for the idea and
action for every flag.
Two open bullets with identical text cannot be told apart by a substring query, so neither can be
closed until one is edited. This is not specific to starting; action add twice does the same.
Complete an action
Completing matches a bullet by substring, ticks it off, and logs it to today’s journal. If the
action had a manifest note, that note is archived to actions/_done/<year>/ and becomes append-only:
cdno action complete --project surrogate-model --query "feature set B"
Drop an action
Not everything on the list gets done. When an action is superseded, abandoned or reprioritised, drop it rather than completing it:
cdno action drop --project surrogate-model --query "demo proposal" \
--reason "superseded by the demo-planning action"
This matches and archives exactly like complete, but the note is stamped status: dropped with no
completion date, and the journal records action dropped on … instead of action done on ….
The distinction matters more than it looks. The daily log is what the weekly review, the monthly scan and every later verdict read back from — so completing work that was never performed leaves your own vault asserting something untrue, and the only repair is a correction line written by hand. A dropped action also stays out of the completed-actions views, because it carries no completion date.
--reason is optional and worth giving. “Superseded by X” and “no longer wanted” are different
facts, and only one of them tells you to go looking for the replacement.
Inline vs. manifest — when to promote
| Use an inline bullet (default) | Promote to a manifest note |
|---|---|
| A discrete next step | An investigation spanning multiple days |
| Done in one sitting | Produces evidence / artefacts to link |
| No supporting detail needed | Needs success criteria, status, or its own notes |
Keeping the default cheap is deliberate — it means capturing the next step never costs more than a sentence. See The Research Logbook Method for the rationale.
Next: Commitments and deadlines.
Renaming a note
A note’s slug is its file name, and every [[wikilink]] and every project: field types it.
Sometimes the slug you got is not the one you want to live with. This tutorial renames a project
whose derived slug stopped mid-title, follows what the rename changes, and covers what to do on a
second machine. The reference is cdno note rename.
How you get a bad slug
The slug is derived from the title, and a long title is cut at 50 characters:
cdno project create --title "Calibrate the surrogate model against the high-fidelity thermal runs" --context work
# -> projects/calibrate-the-surrogate-model-against-the.md
Every later link now has to say [[calibrate-the-surrogate-model-against-the]]. The way to avoid
this is to name the note when you create it:
cdno project create --title "Calibrate the surrogate model against the high-fidelity thermal runs" \
--context work --slug surrogate-calibration
Every create verb takes --slug (see the slug rule),
and an agent creating a note over MCP is told to propose a short one. For a note that already
exists, rename it.
Preview first
Give the project an action note and start it, so that the journal and the action note both point at the project:
cdno action add --project calibrate-the-surrogate-model-against-the --title "Run the baseline sweep" --note --energy deep
cdno action start --project calibrate-the-surrogate-model-against-the --query baseline
Then preview the rename:
cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration --dry-run
Dry run: nothing written.
move projects/calibrate-the-surrogate-model-against-the.md -> projects/surrogate-calibration.md
rewrite actions/run-the-baseline-sweep.md 1 field (project:)
rewrite journal/2026/daily/2026-11-06.md 3 links
alias projects/surrogate-calibration.md gains `calibrate-the-surrogate-model-against-the`
log journal/2026/daily/2026-11-06.md
renamed [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
was: calibrate-the-surrogate-model-against-the
4 references in 2 notes · plan 2109…
Your plan hash will differ: the log lines carry their time stamps.
Each line is one change: the file moves, two other notes have references corrected (three links in
the journal, from the lines for the project’s creation, the action and the start; the project:
field of the action note), the project gains an alias, and today’s log gets
one entry. Nothing is written. If the preview is refused, it is refused exactly as the write would
be, so a dry run is a real check.
Write it
cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration \
--reason "the derived slug stopped mid-title"
With the note and --to given, the command writes as soon as it has checked the plan, without a
confirm, in a terminal or not; the dry run was your look, and it is advice, not a lock. Leave out
--to in a terminal and you are asked for it, shown the preview and asked to confirm. That confirmed
write is tied to the preview: if a note it would rewrite changed while you decided, it refuses with
stale_plan and writes nothing. Over MCP the guard applies when the agent passes the preview’s plan_hash as expected_plan.
What changed
The project map has a new aliases: entry for the old name:
aliases: [calibrate-the-surrogate-model-against-the]
and the old name still works. cdno open calibrate-the-surrogate-model-against-the finds the
project, and says it was found by its former name. cdno project show takes it too, and prints the
current slug in its header.
Today’s log gains one entry:
- **09:44**: renamed [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
was: calibrate-the-surrogate-model-against-the
reason: the derived slug stopped mid-title
was: is plain text on purpose. It records what the note was called, and a link there would be
rewritten by the next rename. Every earlier state on [[…]] and started [[…]] line in the journal
now names surrogate-calibration, so the project’s history is still one search, and a link clicked
in an editor lands on the note.
This is the one place the vault edits the past. The log is append-only: what was recorded and when
never changes. A reference is an address, not content, and correcting an address changes neither
(see the method page). cdno lint accepts exactly that and nothing else in an
archived note.
Renaming back
Rename old to new and back to old and you get aliases: [new]. The first renamed line’s
link is rewritten to [[old]] like any other link to the note, and its was: old stays as
written; the second reads renamed [[old]] … was: new.
When it is refused
Nothing is written on a refusal, and each has a code (the table says what to do for each). The common ones: the new slug is invalid or taken, the note is a kind that cannot be renamed (a daily note, an archived action; see which notes can be renamed), or the vault is behind its upstream.
The other machine
If the vault is a git repository synced between machines, the other one does nothing special: the renamed files, the corrected links and the alias arrive through the sync like any edit.
- Rename on a machine that is up to date. A rename refuses with
behind_upstreamwhile the vault is behind, and--skip-sync-checkoverrides that only when you know nothing unpushed on the other side touches these notes. - If the sync stops on a conflict, resolve it, then run
cdno lint. A link that came back with the old name still works through the alias, and lint warns that[[old]]names a renamed note, nownew. Leaving it is harmless;cdno note rename new --relinkrewrites every such link to the new name in one go (relinking the stragglers), and editing one by hand is allowed too, even in a past log entry. - Run any
cdnocommand in the vault once on every machine, on a cdno that hasnote rename, before renaming anything an archived action references; the reference says why and whatcdno lintsays otherwise.
Over MCP
An agent previews with rename_note { dry_run: true }, shows you the plan, and writes with the
plan’s plan_hash as expected_plan. See Renaming a note.
Commitments and deadlines
A commitment is a dated promise — to others, or a hard promise to yourself. Cuaderno keeps these distinct from your to-do list and gives you one aggregated view of everything with a deadline, wherever it lives.
The aggregated view
cdno commitments # everything due, sorted by date, overdue flagged
cdno commitments --weeks 6 # look six weeks ahead instead of the default two
cdno commitments is a computed view. It always includes a
30-day overdue look-back on top of the lookahead window, so nothing slips silently into the past. It
draws from four sources (see Business rules):
- Project milestones marked
--hard, of an open (active or parked) project. - Stewardship periodic commitments (recurring dashboard lines).
- Standalone commitment notes (below).
- Action notes with a self-imposed
due:not tied to a milestone, of an open (active or parked) project.
A project’s own dates (the first and last) count while the project is open, active or parked: a hard date on a parked map is still a promise. Completing or dropping a project takes its milestones and action due dates out of the view until it is activated again. A standalone commitment that names a parked or closed project keeps surfacing, since it is a promise to someone else.
Standalone commitments
For a one-off promise that isn’t naturally a project milestone or a recurring stewardship line:
cdno commit create --title "Pay rent" --due 2026-06-01 --context personal
# -> commitments/pay-rent.md
# Optionally attribute it to a project or stewardship:
cdno commit create --title "Review Erik's draft" --due 2026-05-20 --context work --project projects/icml-paper
When it’s fulfilled, mark it done — it’s stamped and moved to commitments/_done/<year>/:
cdno commit done --slug pay-rent
When a promise ends without being kept
Not every promise is kept. The client cancels, the plan changes, the thing is overtaken by events.
Marking it done would be the wrong record — cdno commitments and every weekly and monthly review
read the daily log back, and a completion says the promise was fulfilled:
cdno commit drop --slug quarterly-report --reason "the client cancelled the engagement"
The note is archived beside your completed ones, stamped status: dropped with no completion date,
so it never counts as finished work. --reason is optional; a typo needs no explanation, a
cancellation usually deserves one.
If the date merely moved, reschedule is the
verb you want — it records the move rather than ending the promise.
Nothing is destroyed by a drop: the note keeps its body and its created date. But a drop is
re-decided rather than undone — there is no un-drop. If the promise comes back, make it again, and
the record honestly shows both decisions.
Deadlines that live elsewhere
You often don’t need a standalone note — put the deadline where the work is:
# A hard project milestone shows up in `cdno commitments`:
cdno project milestone add --slug icml-paper --title "Camera-ready" --date 2026-02-01 --hard
# A recurring obligation on a stewardship dashboard:
cdno stewardship add-periodic --stewardship finances --title "File quarterly taxes" \
--every "every 3 months" --next 2026-07-15
Both surface in the same aggregated list, so cdno commitments is the single place to answer “what
have I promised, and when?”.
Next: Stewardships and tracking.
Stewardships and tracking
A stewardship is a small, bounded, perpetual responsibility — your health, your finances, a
service you maintain. Unlike a project it never “finishes”; you just tend it. Stewardships can carry
recurring commitments and, when expanded, time-series tracking. Verbs:
cdno stewardship and cdno track.
Two shapes: flat and expanded
# Flat — a single dashboard file, no tracking. Good for "finances".
cdno stewardship create --name "Finances" --context household
# Expanded — a folder with room for tracking/ and routines/. Use --tracking.
cdno stewardship create --name "Health" --context personal --tracking
A flat stewardship is stewardships/<slug>.md. An expanded one is stewardships/<slug>/ with an
_index.md, a tracking/ subfolder for entries, and a routines/ subfolder for reference docs
(workout plans, checklists — not logs). Only expanded stewardships accept tracking entries.
See what you’re tending
cdno stewardship list # each one's variant, tracking count, staleness badge
cdno stewardship show --slug health
Periodic commitments
Recurring obligations attached to a stewardship show up in the aggregated commitments view:
cdno stewardship add-periodic --stewardship health --title "Dental check-up" \
--every "every 6 months" --next 2026-09-01
--every takes a recurrence: daily, weekly, monthly, yearly,
or every N months.
Tracking entries
For habits and metrics on an expanded stewardship, file a tracking note. The activity is
positional and selects the template — a vault’s .cuaderno/templates/tracking-<activity>.md if you
have one, else a generic fallback. (Ready-made variants live in the repo’s
examples/templates/tracking/; see Customising templates.)
cdno track gym --stewardship health --content "Upper body A; RDL up to 25kg"
cdno track body --stewardship health --content "Weight 78.4kg, resting HR 54"
cdno track swim --stewardship health --content "1500m, 28min"
-
--stewardshipcan be omitted when there’s exactly one expanded stewardship — Cuaderno defaults to it. With more than one, it’s required. -
--routinelinks a reference doc from the stewardship’sroutines/folder into the entry — but only when the resolved template has aroutine:field (thegym.mdexample variant does; the generic default has none, so it no-ops there). -
--contentis optional; leave it empty and fill the entry’s tables in afterward. -
--atfiles the entry for a day that has already passed. Recording lags the event more often than not — a statement reconciled at the weekend, a reading taken this morning and typed up tonight — and without it the entry lands on the wrong day and the trend bends.cdno track body --stewardship health --at 2026-04-06Filing is always journalled to today’s daily log, naming the day the entry describes, so a backfill stays visible in the record rather than quietly appearing in the past. Dates more than 50 years back or a year ahead are refused — that far out is a typo, and a typo would reshape a trend without saying so.
Numbers belong in frontmatter
A number written into the body is prose; a number in frontmatter is data. An agent files them
through the MCP create_tracking_entry tool’s metrics parameter, and each key becomes a
frontmatter key:
---
type: tracking
stewardship: finances
activity: savings
date: 2026-07-25
balance: 12480.50
contributed: 400.00
---
When one entry holds several comparable items — three subjects practised, four categories spent against — write a sequence of flat records rather than one key per item, so the same subject can recur within the entry:
detail:
- { minutes: 25, subject: harmony }
- { minutes: 20, subject: harmony }
- { minutes: 15, subject: sight-reading }
A metric that reports a level rather than a total — a balance, a measurement, the last set of
the day — reduces to the last record in the entry, and “last” means the order the records appear in
the file. If you append them out of order, give every record an at field and they sort by it:
detail:
- { balance: 1200, at: "09:00" }
- { balance: 1240, at: "18:00" }
It is all-or-nothing: if any record lacks an at, or carries one that does not parse (09:00,
9:00, 9:00 AM and their with-seconds forms all do), the file’s own order stands and nothing is
reordered. The key is at rather than time precisely because time is a plausible metric —
a swim split, a lap time — and ordering a record set by one of its own measurements would report
a number that was never the last reading.
Write the colon. at: 18:00 needs no quoting — the colon is what keeps YAML reading it as text —
but at: 1800 is a number and never reaches the time parser at all.
Falling back to file order is the safe answer — ordering a half-stamped set would have to invent
a position for the unstamped records, and whichever position it invented would quietly change
which reading a last metric reports. But the fallback used to be invisible, so a set stamped
morning / evening looked identical to one that sorted correctly. cdno lint now reports an
at it cannot use, and a set where only some records carry one, naming the note and the value.
An entry with no at anywhere is never reported: that is the normal way to write these notes.
Declaring a metric under [schemas.tracking.fields] (see
Configuration) gets it type-checked on the way in — a float for
a measurement or an amount, an int for a count. Anything undeclared is written as given, except
the four keys that identify the note — type, stewardship, activity, date — which a metric
may not name. They are the note’s identity rather than data about it, and the engine owns them:
date is fixed by the filename, and activity is what every reader groups by.
Tracking entries are append-only — they’re your historical record.
Read them back over a window via the MCP get_stewardship_tracking tool, or with
cdno search.
From entries to series
Filing an entry is half the story. The other half — how those numbers turn into a chart, and why a
series sometimes shows a number that means nothing — comes down to two things: which aggregate
each metric declares, and which source, body table or frontmatter, the activity is read from.
Every [tracking.<activity>] key is documented in full in the
Tracking section of the configuration reference; this is
the plain-language version of why those keys exist.
The metric-kind taxonomy
Every number you track is one of four kinds, and the kind decides the aggregate it wants:
| Kind | Examples | Aggregate |
|---|---|---|
| Total | amount spent, pages read, minutes practised | sum |
| Level | account balance, a measurement, a top set | last, or max for a high-water mark |
| Rate or rating | a score out of ten, perceived difficulty | mean |
| Occurrence | a call, a visit | none — declare no metrics; the entry’s existence is the record |
Summing is right for exactly one of these — a total — and wrong for the rest. It’s wrong for a level because it adds successive readings of one quantity instead of reporting the quantity: three statements through a spending-free month, each reading a balance of roughly 12,480, would sum to over 37,000 — a number nothing in the account corresponds to. It’s wrong for a rating because the series grows with how often you record it, not with how the session actually went — log twice a day instead of once and the total doubles for no reason but the logging cadence. An occurrence doesn’t want a number at all: declaring the activity with no metrics is a complete, valid use — the record is “this happened”, not “how much”.
Two worked examples
A scalar activity, a level. The savings activity shown above has no repeated records — every
entry is one reading. balance is a level, so it wants last, not the default sum; contributed
is a total, so the default is already right:
[tracking.savings]
# no `records` — this activity's metrics are scalars read straight off the entry
[tracking.savings.metrics.balance]
aggregate = "last" # a LEVEL — the reading itself, not a running total of readings
unit = "EUR"
[tracking.savings.metrics.contributed]
aggregate = "sum" # a TOTAL — money added since the last entry
unit = "EUR"
Filed against the frontmatter shown earlier (balance: 12480.50, contributed: 400.00), this
produces two series: savings · balance, whose points are each entry’s balance verbatim, and
savings · contributed, whose points are the sum of every contribution recorded on that date.
A record-based activity, two aggregates over the same records. practice splits by subject
and tracks two different kinds of number per session:
[tracking.practice]
records = "detail" # frontmatter key holding the repeated records
group_by = "subject" # one series per distinct subject
[tracking.practice.metrics.minutes]
type = "int"
aggregate = "sum" # a TOTAL — minutes practised
unit = "min"
[tracking.practice.metrics.focus]
aggregate = "mean" # a RATING — sum would grow with how often you log, not with how focused you were
One entry covering two subjects in the same sitting:
---
type: tracking
stewardship: study
activity: practice
date: 2026-07-25
detail:
- { minutes: 25, focus: 8, subject: harmony }
- { minutes: 20, focus: 6, subject: harmony }
- { minutes: 15, focus: 9, subject: sight-reading }
---
Four series come out of it, named <activity> · <group> · <metric>:
practice · harmony · minutes= 45 (25 + 20, summed)practice · harmony · focus= 7 (the mean of 8 and 6)practice · sight-reading · minutes= 15practice · sight-reading · focus= 9
Same records, same date, two different reductions — because minutes and focus are different
kinds of number, not because one config option was chosen for the whole activity.
Gaps, not zeros
A subject you didn’t practise on a given day produces no point on that day, not a zero.
Zero-filling would draw a false line down to the axis — a session that didn’t happen is not the same
as a session scored zero. The same rule means a brand-new subject needs no configuration change: the
first time ear-training shows up in a detail record, its series starts there, on that date, with
every date before it correctly absent rather than zero.
Which source a series comes from
An undeclared activity — no [tracking.<activity>] for it in config.toml — is read from the
first table in the note’s body: one series per column, each column’s numeric cells summed. That’s
right for a rep count and wrong for almost everything else, which is why the declared path above
exists. If you’re still filing through a table, see
Wide, not long
in the example templates — a table column is a series, and a Metric / Value row shape sums
unrelated numbers into one meaningless total.
Declaring an activity moves it to frontmatter: once the declaration yields a series for that activity, its body tables are no longer read — every note of it, not just the ones carrying frontmatter metrics. So the same metric can never appear twice under two disagreeing numbers, one from a table and one from the declared reduction. It also means a half-migrated activity shows only what its frontmatter carries, so migrate an activity’s notes together rather than one at a time.
Next: Weekly review.
Weekly review
The weekly review is the one slightly-longer ritual in the method — a short retrospective plus a single goal for the week ahead. It’s where you celebrate progress, notice what’s stuck, and set direction without micromanaging.
The weekly note
Each ISO week has a weekly note with four sections:
- Wins — what moved (actions completed, evidence filed, projects advanced).
- Challenges — what got in the way.
- One Improvement — a single thing to change next week.
- This Week’s Goal — the week’s anchor: one goal everything else orbits.
View the current week’s note (or any week) any time:
cdno weekly # this ISO week
cdno weekly --date 2026-04-20 # the week containing that date
The guided ritual
cdno review weekly walks you through it. Interactively, it prompts for
each retrospective section and writes them into this week’s note, then asks for next week’s goal
and sets it as the This Week’s Goal of next week’s note — so when the new week starts, its
anchor is already there.
cdno review weekly
Run non-interactively (or with --no-interactive), it reads the current note rather than prompting —
handy for scripts or a quick scan.
A weekly cadence that works
A simple routine, ~15 minutes:
cdno status— skim active projects and their top actions.cdno commitments --weeks 2— what’s due in the next fortnight?cdno stewardship list— any habit looking stale?cdno review weekly— capture Wins / Challenges / One Improvement, set next week’s goal.
The point isn’t a perfect record; it’s a regular moment to look up from the work, acknowledge what you did, and choose one direction. Parking, completing or dropping a project here, or retiring a question, is a good outcome, not a failure.
A question per relationship
Not every open project is asked the same thing, because not every project is yours to move. Walk
the active projects (cdno project list shows which name a holder) and ask each the question its
relationship implies:
- Yours (no holder): did it move? If it sat still, is it still the priority, or should it wait? Parking it is a fine answer.
- Handed off, and its map changed this week: what came back, and what is waiting on my decision? Not “did it move?”: the moving is someone else’s.
- Handed off over an empty Waiting On: a name over an empty Waiting On is a parked map wearing a name. Park it, or take it back.
- Handed off, but you worked on it yourself on two or more days this week: doing a thing on a handed-off map does not make it yours, doing most of its work does. Take it back, or change the name to whoever really has it.
- Handed off, and stuck for weeks (at the monthly review, from its list of stuck projects): chase the holder, or take it back.
A handed-off project that did not change this week needs no question at all. The questions are prompts, never verdicts, and a hand-off is never an answer to a full cap: to make room, park, complete or drop a project. See Managing projects.
With Claude
The MCP tools get_weekly_context, read_weekly_note, and upsert_weekly_section let an assistant
run the same review conversationally — reading your week back to you and writing the sections as you
talk. get_weekly_context carries, per active project, counts, held_by, changed_this_week,
waiting_on_empty and focus_days_while_held, so an assistant can ask each project the question
above (the stuck-project question reads get_monthly_context’s stuck_projects, not these fields); the weekly-review example skill shows how. See Connect to Claude.
Next: Inbox and triage.
Inbox and triage
The inbox is a pressure-release valve: capture a thought now, decide what to do with it later. This keeps the daily loop friction-free — you never have to stop and classify something mid-flow.
Capture
cdno capture "does Chen 2025 use the same preconditioner?"
cdno capture "ask IT about the cluster quota"
Each capture becomes a small slug-named note in inbox/. That’s the whole cost — no fields, no
decisions. Capture liberally.
Triage
When you have a moment (a natural fit for the weekly review), process the inbox:
cdno triage
Interactively, triage walks each pending capture and offers, for each one, to:
- keep it as a project action — turn it into a next action on a project, or
- discard it — it served its purpose, or
- skip it — leave it for next time.
Run non-interactively (or with --no-interactive), triage just lists what’s pending — a quick
way to see the backlog without acting on it.
A healthy inbox is empty-ish
The inbox is a transit point, not a filing cabinet. The goal isn’t zero at all times — it’s that nothing important lives only there. Anything worth keeping becomes an action, a piece of evidence, a question, or a log line; the rest gets discarded without ceremony.
With Claude
The MCP tools capture, triage_inbox (lists pending items), and discard_inbox_item let an
assistant capture on your behalf and help you clear the backlog conversationally.
Next: Searching your vault.
Searching your vault
Cuaderno keeps a full-text index of every note, so you can find anything fast — ranked best-first,
with filters by type, date, and portfolio. The command is cdno search.
The index covers each note’s title as well as its body, and a title match counts for ten times a body match. So searching for words you remember from a note’s heading surfaces that note first, even if a dozen other notes mention the same words in passing — there is no separate “search by title” command to remember.
If you already know exactly which note you want, cdno open takes you
straight there by slug, date, or path, without ranking anything.
In a terminal, cdno search also follows its results with a picker: choose a hit and it opens in
your editor. The results print first either way, so piping or redirecting the output is unaffected.
Basic search
cdno search "preconditioner"
cdno search "sparse attention" # multiple words are ANDed
The query is matched case-insensitively and terms are combined with AND. Quotes and operators are treated as literal words, not search syntax. Results come back ranked, best match first.
Filter the results
# Only one note type:
cdno search "ablation" --type evidence
# Within a date window (inclusive):
cdno search "mesh" --from 2026-03-01 --to 2026-03-31
# Only inside one portfolio:
cdno search "speedup" --portfolio sparse-vs-dense-attention-ood
# Cap the number of hits (default 20):
cdno search "todo" --limit 5
Filters combine, so you can scope tightly:
cdno search "preconditioner" --type evidence --portfolio sparse-vs-dense-attention-ood --from 2026-01-01
Scripting with --json
--json returns the ranked hits as a JSON array — each with path, note_type, title, snippet,
and score — ready for jq or another tool:
cdno search "speedup" --json | jq -r '.[].path'
See JSON output for the exact shape.
When search feels stale
Search reads the SQLite index, which is reconciled automatically on every run. If results ever look out of date (e.g. after a bulk external edit), rebuild it explicitly:
cdno reindex
The Markdown files are always the source of truth; the index is just a rebuildable cache (see Business rules).
Next: Customising templates and frontmatter — tailor how notes are scaffolded and which frontmatter fields they require.
For exhaustive detail on any command, see the CLI reference.
Customising templates and frontmatter
When cdno scaffolds a note it fills a template. Every note type has a built-in template, and you
can override any of them per-vault — to change the structure, the default sections, or the frontmatter
fields. You can also require extra frontmatter fields so cdno lint
keeps your notes consistent. This tutorial walks through both, hands-on.
What’s covered here is the shipped behaviour. Both kinds of config variable now resolve in custom templates: static
[variables]and interactive[variables.prompt](gathered from a TTY prompt or a--var name=valueflag).
Where templates live
Templates live in .cuaderno/templates/, one Markdown file per note type (e.g. project.md,
evidence.md). cdno resolves the effective template at creation time:
- a custom variant file —
<type>-<variant>.md(trackinguses this: the variant is the activity slug, e.g.tracking-gym.md), then - a custom type file —
<type>.md, then - the built-in variant default, then
- the built-in type default.
So a custom file in .cuaderno/templates/ always wins over the built-in. No activity-specific
variants ship built-in (tier 3 is empty today) — tracking variants are entirely yours to add;
see Tracking variants below.
cdno initwrites two starter templates —.cuaderno/templates/daily.md, andconcept.mdfor theconceptcustom type it declares. Every other built-in type uses its built-in default until you add a file for it. The quickest way to get an editable copy of a built-in iscdno templates eject <type>— e.g.cdno templates eject projectwrites.cuaderno/templates/project.mdmatching the built-in, ready to edit. You can also just create the file yourself, as the next section shows.
Customise a template
Say you want every project to start with a ## Risks section. Eject the built-in as a starting point:
cdno templates eject project # writes .cuaderno/templates/project.md
That writes the full built-in template. Insert a ## Risks section (you can reference any of the
{{placeholders}} that cdno templates vars project lists) and leave the rest as-is, so
.cuaderno/templates/project.md reads:
---
type: project
context: {{context}}
status: {{status}}
created: {{created}}
core_question: {{core_question}}
closed: null
---
# {{title}}
## Current State
New project. No work done yet.
## Risks
## Next Actions
- [ ] Define first concrete step (light)
## Waiting On
(nothing yet)
## Milestones
- [ ] First milestone — target: TBD
## Links
- Portfolio: (none yet)
Now create a project:
cdno project create --title "Surrogate model" --context work
The new projects/surrogate-model.md follows your template — including the ## Risks section:
---
type: project
context: work
status: active
created: 2026-06-30
core_question: null
closed: null
---
# Surrogate model
## Current State
New project. No work done yet.
## Risks
## Next Actions
- [ ] Define first concrete step (light)
## Waiting On
(nothing yet)
## Milestones
- [ ] First milestone — target: TBD
## Links
- Portfolio: (none yet)
cdno templates eject <type> is the recommended way to get an editable base — it matches the
built-in at the moment you eject. (You could instead hand-write the file, or shape it from a note
cdno already created, but eject saves the guesswork.) If a later release adds a frontmatter key to
the built-in, your copy does not gain it by itself: cdno lint and cdno templates list say so, and
cdno templates sync <type> adds it without touching your edits.
Editing a template only affects notes created afterwards — existing notes are untouched. (And
cdno normaliseonly reorders frontmatter keys; it won’t add a new section like## Risksto old notes.)
closed:in a customised project template. The built-in project template ends its frontmatter withclosed: null, the date a project is completed or dropped. A.cuaderno/templates/project.mdyou ejected before that key existed does not gain it by itself, and nothing breaks if it never does: closing a project writes the key whenever it is missing.cdno lintandcdno templates listsay when your template lacks it, andcdno templates sync projectadds it next to its built-in neighbour (aftercore_question:when you kept that) without touching anything else. The same command covers any key a future release adds to a built-in template.
Tracking variants
tracking is the one type whose template is chosen per activity. cdno track <activity>
slugifies the activity and looks for .cuaderno/templates/tracking-<activity>.md, falling back to
the generic tracking template when there’s none. So a custom .cuaderno/templates/tracking-gym.md
gives cdno track gym a bespoke layout without touching cdno track swim, and
.cuaderno/templates/tracking.md overrides the generic fallback for everything else.
Only the neutral generic template ships built-in — no activity-specific variants are baked into
the product. Ready-made gym, body, and swim variants (exercise table, body-metrics table,
swim-set table) live in the repo under
examples/templates/tracking/;
copy one to .cuaderno/templates/tracking-<activity>.md to use it, or start your own from it.
Template variables
Templates use {{placeholder}} markers. cdno substitutes the values the note’s creation command
supplies. Two rules to know:
- An omitted optional value renders as
null(e.g.core_question: nullabove when you don’t pass--question). - An unknown placeholder is left verbatim —
{{nope}}stays as the literal text{{nope}}in the note. So a template should only use the placeholders its note type actually provides.
Each type provides these:
| Note type | Available {{placeholders}} |
|---|---|
daily | date, heading, weekday, day_name, week |
weekly | week, week_num, year, date_start, date_end |
monthly | month, month_name, year, date_start, date_end, weeks |
project | title, context, status, created, core_question |
action | title, slug, project, energy, status, created, due, completed, milestone, criteria, blocker, tags |
portfolio | question, project, created |
evidence | source, origin, portfolio, content, created |
stewardship | name, context |
tracking | stewardship, activity, activity_title, routine, content, date, date_long |
question | question, domain, created, updated |
commitment | title, context, status, due, project, stewardship, created, completed |
inbox | body, created |
You can use any subset, in any order, and add as much static Markdown around them as you like.
Daily specifics.
weekdayandday_nameare aliases for the same value — the weekday name (e.g.Sunday) — so use whichever reads better.weekis the ISO-week labelYYYY-Www(e.g.2026-W27), matching the weekly note for that date, so[[{{week}}]]in a daily template links straight to it.
Discover them from the CLI.
cdno templates vars <type>lists exactly this table for a type — the complete set its create path supplies — and folds in any[variables]/[variables.prompt]names your config adds, classified by source. For examplecdno templates vars tracking. See thetemplatesreference.
Static config variables
Beyond the per-type placeholders above, a custom template can reference vault-wide static
variables you define under [variables] in .cuaderno/config.toml. These resolve on every note
type. For example:
# .cuaderno/config.toml
[variables]
author = "A. Researcher"
institution = "University of Examples"
A custom template can then use {{author}} / {{institution}} and they’ll be substituted at
creation. Precedence: a per-type (contextual) placeholder of the same name always wins over a config
variable, so config vars only fill names the note type doesn’t already supply.
Prompted variables
A static variable is the same on every note. When you want a value that changes per note — a ticket
id, a collaborator, a meeting code — declare it under [variables.prompt], where the value is the
prompt message:
# .cuaderno/config.toml
[variables.prompt]
ticket = "Ticket reference?"
Reference it in a custom template like any other placeholder (e.g. ticket: {{ticket}} in the
project frontmatter). When you create a note whose effective template uses a prompted variable, cdno
gets the value one of three ways:
--var name=valueon the command (repeatable), e.g.cdno project create --title T --context work --var ticket=ABC-123;- otherwise, in an interactive TTY,
cdnoasks (“Ticket reference?”) and shows the value in the confirm preview before writing; - otherwise (non-interactive, no
--var) it errors rather than writing a note with a literal{{ticket}}:
Error: missing value for template variable 'ticket' (pass `--var ticket=value`, set a default under
[variables] in .cuaderno/config.toml, or run interactively in a TTY)
--var is available on every note-creating command: project create, question create,
stewardship create, commit create, portfolio create, file, track, action add --note, and
action promote.
A few rules worth knowing:
- A prompted name that also has a static
[variables]default is satisfied by that default — you’re not asked, and it won’t error. (The static default wins by precedence, so--varcan’t override it; remove the default if you want to be prompted.) - A
[variables.prompt]entry whose{{name}}your template doesn’t actually use is ignored. - The same precedence applies: a per-type placeholder of the same name wins over a prompted variable.
--varonly applies to templated notes.cdno file --attach(the attachment stub) and a plainaction add(no--note) aren’t templated, so--varis ignored there.- The implicit-write paths — daily (
log), weekly, and inbox (capture) notes — don’t gather prompted values, and neither do MCP-driven creations (there’s no--varover MCP). A[variables.prompt]placeholder in one of those templates fails at creation (anUnresolvedPromptserror) instead of being asked for; give it a static[variables]default instead.
Frontmatter field order and normalise
Your template also defines the canonical order of frontmatter keys for that type. Notes cdno
creates are already in that order; for hand-authored or migrated notes,
cdno normalise reorders their frontmatter to match the template
(--check reports without writing). So if you reorder the keys in project.md, a later
cdno normalise brings older project notes into line.
Require extra frontmatter fields
Beyond the built-in required fields, you can demand vault-specific ones per type with a
[schemas.<type>] section. For example, to require every project to name an owner, add to
.cuaderno/config.toml:
[schemas.project]
extra_required = ["owner"]
This is enforced by cdno lint, which now errors on any project
missing the field (a missing key, or one whose value is null, fails):
cdno lint
# [error] projects/surrogate-model.md: missing required field `owner` for note type `project`
# Error: found 1 error(s), 0 warning(s)
lint exits non-zero on errors, so this is a good gate to run in a pre-commit hook or CI over a
git-tracked vault.
Satisfy the requirement going forward
Add the field to your template so new notes carry it. Give it a non-null default you can edit per
note (an empty key — owner: — is YAML null and still fails the lint; use a placeholder value):
---
type: project
context: {{context}}
status: {{status}}
created: {{created}}
core_question: {{core_question}}
closed: null
owner: unassigned
---
New projects are now born with owner: unassigned (edit it as needed) and pass the lint.
Existing notes aren’t changed retroactively — fix them by adding the field, then re-run
cdno lint until it’s clean.
Required fields are about presence, not value: any non-null value satisfies the check. Combine
extra_requiredwith a template default and the occasionalcdno lintand your vault stays uniform without any per-note ceremony.
Give a field a type
extra_required only checks that a key is present. When you want cdno lint to also check the
value — that meds is a boolean, mood is one of a fixed set, since is a real date — declare a
typed field instead, under [schemas.<type>.fields.<name>]:
[schemas.daily.fields.meds]
type = "bool"
default = false
[schemas.daily.fields.mood]
type = "string"
values = ["low", "ok", "good"]
Now a daily note whose meds: isn’t a boolean, or whose mood: isn’t one of the three allowed
values, gets a cdno lint warning. Typed fields are also recognised by cdno templates,
so a custom template referencing {{meds}} no longer warns that it “renders literally”.
A typed field’s default is populated at create, too: add meds: {{meds}} to your custom
daily.md and every new daily note is scaffolded with meds: false — the declared default — rather
than a literal {{meds}}. A field with no default renders null. (As with any placeholder, the
field only appears in a note if the template references it; and if the note’s create path already
supplies that name, or a [variables] static var does, that value wins over the default.) See
Typed schema fields in the configuration
reference for the full grammar and its limits.
Edit templates from the CLI
cdno templates covers the whole loop without leaving the terminal.
cdno templates list # every type, which template is in effect, and where
cdno templates show project # the effective content, verbatim
cdno templates eject project # copy the built-in default into .cuaderno/templates/
cdno templates save --note-type project # write it: --file, --file - for stdin, or $EDITOR
cdno templates sync --all # add keys a release added to the built-ins, keep your edits
cdno templates new --note-type people # scaffold a starter for a custom type
list names the source in effect for each type — builtin_default, custom_base, a variant, or
none for a custom type with no template yet. save on a built-in creates the override for you,
so it needs no prior eject, and show is byte-verbatim, which means the round trip holds:
cdno templates eject project
cdno templates show project | diff - .cuaderno/templates/project.md # no output
See the templates reference for every flag.
See also
- Configuration — the configurable surface.
- Configuration reference — every
config.tomlkey. - Frontmatter fields — the built-in fields per note type.
normalise,lint.
Tracking people
Cuaderno’s built-in types don’t include a “person” — deliberately, since a person decomposes into the notes you already keep (daily logs, meetings, actions, commitments). But if you want to answer questions like:
- What was my last interaction with Jane?
- When did I ask Jane to do something — and what did she ask me?
…a custom note type plus a linking convention gets you there,
with no bespoke CRM. This recipe is the worked example; the files live in
examples/note-types/person/.
1. Declare the type
Add to .cuaderno/config.toml:
[note_types.person]
folder = "people"
That’s the minimum — a person’s identity is the note title. (Add required/optional fields if you
want enforced frontmatter; see the reference.)
Optionally copy the example person.md
to .cuaderno/templates/person.md for a note with a ## Log section. Without a template, Cuaderno
synthesises a minimal note.
2. Create people
cdno note create person --title "Jane Smith"
This writes people/jane-smith.md. cdno note list person enumerates them; cdno lint keeps them
honest.
3. Link people from your notes
The key habit: whenever a person shows up in a daily log, a meeting section, or an action, reference
them with a [[people/<slug>]] wikilink:
## Logs
- **14:30**: standup — [[people/jane-smith]] asked me to review the sparse-attention draft
Body wikilinks are indexed, so every mention becomes a backlink on the person’s note, and the raw text is full-text searchable.
4. Answer the questions
“What was my last interaction with Jane?” — the reliable answer is the top line of her person
note’s ## Log, which you keep most-recent-first. To scan across the vault, search her slug:
cdno search "people/jane-smith" --type daily --from 2026-06-01
Search ranks by relevance, not date, so bound the window with --from / --to and read the
dates on the hits (daily notes carry their date in the filename) rather than trusting the order.
Drop --type to include meetings and action notes.
“What did Jane ask me — and what did I ask her?” — this is direction, which lives in your
prose, not in structure. Write the log line so the direction is explicit (“Jane asked me…”, “I asked
Jane…”), and either keep a running ## Log in her person note or let search surface the lines. A
Claude skill can read the dated results and summarise who-owes-what; the structure gives it the
material, the prose gives it the direction.
Why a “person” type and not a built-in
A custom person type is schema-only — it gives you a folder, linting, search, and backlinks,
but no bespoke behaviour. That’s exactly right here: people don’t need caps, state machines, or
aggregation; they need to be findable and linkable. If you later want richer per-person structure,
add fields to the config declaration — no code change. See
Custom note types for the full schema.
CLI reference
cdno is the command-line interface to a Cuaderno vault. This section documents every command. For
the why behind them, see Concepts and Tutorials.
cdno [OPTIONS] <COMMAND>
Run cdno --help for the command list, or cdno <command> --help for any command’s flags.
Global options
These apply to every command:
| Flag | Effect |
|---|---|
--vault <PATH> | Operate on the vault at PATH. Overrides both discovery and CUADERNO_VAULT_PATH. |
--json | Emit machine-readable JSON instead of a formatted table (see below). |
--no-interactive | Never prompt; a missing required argument is an error. Implicit unless both stdin and stdout are TTYs. Also turns off interactive reports. |
--color <WHEN> | auto (default), always, or never. See Colour and interactivity. Spelled --colour too. |
-h, --help | Print help. |
-V, --version | Print the version (top level only). |
Finding the vault
When --vault is not given, cdno discovers the vault by walking up from the current directory
until it finds a .cuaderno/ folder. If that finds nothing, it falls back to the
CUADERNO_VAULT_PATH environment variable. Standing inside a vault always wins over the env var.
See Initialise a vault.
Interactive vs. scripted
Write commands follow one convention (the “flags-and-prompts” pattern):
- Interactive terminal, missing a required flag →
cdnoprompts for it, then asks you to confirm before writing. - Non-interactive (either stream not a terminal — piped, redirected,
< /dev/null, or running in CI, which is the usual reason neither is — or--no-interactive) → a missing required flag is an error. Supply every flag and the command runs unattended.
This means the same command serves a human at a prompt and a script with no changes.
Read commands add one thing on top. In a terminal, several listings offer to open one of the rows
they just printed — cdno project list asks which project you want to see, prints it, and asks
again until you press Esc. The listing itself is printed first either way, so piping, --no-interactive,
and --json all behave exactly as they did before. See
Colour and interactivity.
JSON output
--json makes any supported verb emit structured output:
- Read verbs (
commitments,questions,status,now,orient,search,open, and thelist/showverbs ofproject/portfolio/stewardship, plusaction list) emit their listing or detail object. - Write verbs (
log,capture,file,track, and the create/update verbs ofproject,action,portfolio,stewardship,question,commit) emit a{ "path": ..., "message": ... }result, and run non-interactively (so prompts can’t corrupt the JSON on a terminal).project completeanddropadd what was let go, and print a refusal object and exit1while items are open (see JSON output). migrateemits its report, the plan with--dry-run(seemigrate).- Maintenance / interactive / bootstrap commands (
init,lint,reindex,normalise,triage,review,weekly,monthly) ignore--json.
The CLI’s JSON shapes match the MCP server DTOs. See JSON output for every shape.
Naming a note: --slug
Every verb that creates a note derives its filename, the slug, from the title. For a long title the
derived slug can stop mid-thought (rerun-the-surrogate-model-ablation-on), and it is what every later
[[wikilink]] has to type. The create verbs therefore take an optional --slug <SLUG> to name the
file yourself: project create,
question create,
portfolio create, action add --note,
action promote, note create,
stewardship create and
commit create. The title is unchanged; only the filename is.
An explicit slug must be:
- lowercase
a-z,0-9and single-, with no-at either end; - at most 50 characters;
- not something YAML would read as a number, a boolean, a null or a date (
2026,yes,null,2026-11-05): add a word to it.
An explicit slug is used exactly or refused, never rewritten. If another note already holds the name
(as its filename, or as the folder of a folder note such as portfolios/surrogate-model/) the create
is refused and nothing is written; pick another name. The -2 suffix belongs to derived slugs only.
--slug is never prompted for; omit it and the slug derives from the title. In a terminal a refusal
reads:
$ cdno project create --title "Surrogate model" --context work --slug surrogate-model
Error: creating project
Caused by:
slug 'surrogate-model' is taken by 'projects/surrogate-model.md' (as its stem)
Under --json a refusal is a coded object.
A derived slug takes -2 when the name is held, and a folder note’s name counts as held: a question
titled “Surrogate model” created beside the portfolio portfolios/surrogate-model/ gets
surrogate-model-2, since the bare name would shadow the portfolio’s _index in a
[[surrogate-model]] link. This applies to the verbs that disambiguate: project create,
question create, action add --note and action promote, note create and commit create.
portfolio create and stewardship create keep the raw derived slug (a portfolio’s slug correlates
with its question’s) and refuse a collision outright.
cdno action add --project surrogate-model --note --energy deep \
--title "Rerun the surrogate model ablation on the held-out split with feature set B" \
--slug feature-set-b-ablation
# -> actions/feature-set-b-ablation.md
Renaming a note after the fact is a separate verb, planned under RFC 0007.
The commands
| Command | What it does |
|---|---|
init | Create a new vault |
log | Append a line to today’s daily note, or (log note) a worked-out entry under ## Notes |
capture | Drop a quick note into the inbox |
triage | Process inbox captures |
orient | Morning orientation |
status | Active projects + top actions |
now | What you are in the middle of |
open | Resolve a note reference to its path |
weekly | Show the weekly note |
monthly | Show the monthly note |
commitments | Aggregated deadlines |
questions | List active questions |
search | Full-text search over titles and bodies |
review | Guided weekly/monthly review |
project | Manage project maps |
action | Manage next actions |
portfolio | Manage evidence portfolios |
file | File evidence into a portfolio |
question | Manage question notes |
stewardship | Manage stewardships |
track | File a tracking entry |
commit | Manage standalone commitments |
lint | Validate the vault |
reindex | Rebuild the index |
migrate | Record this cdno’s method version in the vault |
normalise | Reorder frontmatter |
completions | Shell-completion scripts |
cdno init
Create a new vault: the folder tree, a default .cuaderno/config.toml (which declares the concept custom type), and starter daily.md and concept.md templates (every other built-in type uses an in-binary default until you eject one).
cdno init [OPTIONS] [PATH]
Arguments
| Argument | Description |
|---|---|
[PATH] | Target directory. Defaults to the current working directory. |
Options
Only the global options apply. init ignores --json.
Examples
# Create a vault in a new directory:
cdno init ~/notebook
# Initialise the current directory as a vault:
cdno init
It fails if the target already contains a .cuaderno/ directory (it won’t clobber an existing
vault).
See also
- Initialise a vault — what the tree contains and how discovery works.
- Vault structure.
cdno log
Append a log entry to today’s daily note (creating the note if it doesn’t exist yet).
cdno log [OPTIONS] <MESSAGE>
cdno log note [OPTIONS]
Arguments
| Argument | Description |
|---|---|
<MESSAGE> | The log message. Quote it if it contains spaces. |
Options
| Flag | Description |
|---|---|
--at <TIMESTAMP> | Override the timestamp. Accepts YYYY-MM-DDTHH:MM:SS or YYYY-MM-DDTHH:MM. Defaults to now. |
Plus the global options. With --json, emits a {path, message}
result.
Examples
cdno log "scaled the mesh to 2M cells; 4x runtime, still stable"
# Backdate an entry:
cdno log "forgot to record: fixed the sampler seed" --at 2026-04-24T18:30
# Scripted:
cdno log "nightly run complete" --json
# -> { "path": "journal/2026/daily/2026-04-25.md", "message": "Logged to ..." }
Daily notes are append-only — log only ever adds.
A first argument, before any option, that names a subcommand (note) selects it. To log that bare
word as a message, pass it after --: cdno log -- note.
cdno log note
Write worked-out substance (a derivation, a procedure, a page of reasoning) to a daily note as one
### <heading> entry under ## Notes. The pointer line
noted [[journal/<year>/daily/<date>#<heading>]] is written to ## Logs for you in the same
write, followed by the body’s wikilinks in parentheses when it has any. Keep ## Logs for
one-line events and put the substance here.
cdno log note [--heading <STRING>] [--body-file <PATH>] [--date <YYYY-MM-DD>]
| Flag | Description |
|---|---|
--heading <STRING> | The entry’s heading, written as ### <heading> and used as the pointer’s anchor. Prompted in an interactive session. |
--body-file <PATH> | A file holding the entry’s body. Leading blank lines and trailing whitespace are dropped; the first line’s indentation is kept, so an entry may open with an indented code block. A blank body is refused. Opened in your editor in an interactive session. |
--date <YYYY-MM-DD> | The day whose note receives the entry, stamped at the current time. Defaults to today. |
Without --heading or --body-file, an interactive session prompts for the heading and then
opens the body in your editor, and confirms before writing; under --no-interactive (or --json)
each is a missing-flag error.
The heading must be unique within the day, must not reuse a daily section name (Standup,
Intention, Agenda, Meeting, Notes, Logs), and must not contain [, ], |, # or
inline markup (bold, italics, code) nor start with ^. Headings inside the body must be level 3
or deeper and are held to the same uniqueness rule. A refused entry writes nothing.
End an entry that could be reused beyond today with the tag #concept on the body’s last line,
so the review can find it as a candidate for promotion to a concept note.
The command prints Noted <target>, where the target is the entry’s anchored link
(journal/…#<heading>). With --json it emits path, message, target and log_line:
cdno log note --heading "Woodbury identity" --body-file woodbury.md --json
# -> { "path": "journal/2026/daily/2026-04-25.md",
# "message": "Noted journal/2026/daily/2026-04-25#Woodbury identity",
# "target": "journal/2026/daily/2026-04-25#Woodbury identity",
# "log_line": "noted [[journal/2026/daily/2026-04-25#Woodbury identity]] ([[projects/surrogate-model]])" }
Cite the entry elsewhere as [[<target>]].
Focus tag
When an action is in focus, a carried one included,
cdno log and cdno log note append an indented continuation naming its project to the line they
write, so a later review can see where a thought came from:
- **11:12**: checked the mesh
during: [[surrogate-model]]
With nothing in focus the line is written as given. If the focus window cannot be read the line is
written untagged rather than the command failing. The tag sits below the entry’s head, so it never
affects how cdno now reads the log.
Related MCP tool
append_to_log — the same operation for AI clients;
note_to_daily is the counterpart of
cdno log note.
See also
- The daily loop.
- The concept library — why substance goes in
## Notes, and how an entry is promoted to a concept note.
cdno capture
Drop a quick note into inbox/ with a slug-based filename, to be processed later with
triage.
cdno capture [OPTIONS] <TEXT>
Arguments
| Argument | Description |
|---|---|
<TEXT> | The note text. Quote it if it contains spaces. |
Options
Only the global options. With --json, emits a {path, message}
result.
Examples
cdno capture "does Chen 2025 use the same preconditioner?"
cdno capture "ask IT about the cluster quota" --json
When an action is in focus, the new inbox item’s
frontmatter gets captured_during: <project-slug>, recording where your attention was when the
thought arrived. It is the one thing capture adds on its own; with nothing in focus the item is
exactly what you typed.
Capture is meant to be frictionless — no fields, no decisions. Classify later during triage.
Related MCP tool
See also
- Inbox and triage.
triage— process what you’ve captured.
cdno triage
Process uncategorised inbox/ captures. For each one, keep it as a project action, discard it, or
skip it.
cdno triage [OPTIONS]
Options
Only the global options. triage ignores --json.
Behaviour
- Interactive — walks each pending capture and prompts you to keep (turn into a project action), discard, or skip.
- Non-interactive (piped or
--no-interactive) — just lists what’s pending, without changing anything.
A capture’s captured_during tag outlives the file. When triage clears an item, whether you discarded it or kept it as an
action, the log line it writes (triaged inbox item … -- discarded: …) carries the item’s tag as a during: [[<slug>]]
continuation, copied from the item rather than from whatever is in focus at triage time.
Examples
# Work through the inbox:
cdno triage
# Just see the backlog (no changes):
cdno triage --no-interactive
Related MCP tools
triage_inbox (lists pending items) and
discard_inbox_item (clears one).
See also
cdno orient
Daily orientation: the commitments due soon, your active projects, a single suggested starting point, and any stewardship habits that have lapsed.
A habit counts as lapsed when its line in a stewardship dashboard’s ## Active Habits section
declares it so — a status starting with “lapsed” after the em-dash, e.g.
- Swimming 1x/week — lapsed since March. The dashboard is the source of truth (updated
during reviews); orientation only surfaces what it says, without judgement.
cdno orient [OPTIONS]
The suggested start is drawn from the projects that count toward the cap first (active, and naming
no one else as holder; parked projects are not listed at all): a project handed to
someone else (cdno project hand-off) is not your move, so it is
suggested only when no project of yours has an open action, and then with its holder named. Its card
in the project list carries a held by: line.
Projects that share a core question are one undertaking,
so two or more of them read as one card: the question as its title, then one line naming each
project with its next action (held by <name> beside one someone else is moving). A project with no
question, or the only one under its question, keeps its own card. Under --json the same
arrangement is the groups array (see JSON output).
In a terminal this then offers to open one of the projects it just listed, printing what cdno project show would and asking again until you press Esc. Piped output, --no-interactive, and
--json skip the prompt. See Colour and interactivity.
orient does not show what is in focus or the last
thing you paused, and neither field is in its --json: run cdno now for those. The MCP
get_orientation does carry both, because an agent has no other cheap way to know.
Options
| Flag | Description |
|---|---|
--energy <ENERGY> | Bias the suggested starting point toward this energy level: deep, medium, or light. |
Plus the global options. With --json, emits the orientation as a
structured object.
Examples
cdno orient # neutral suggestion
cdno orient --energy deep # bias toward a heavy-focus action
cdno orient --energy light # bias toward something quick on a low day
cdno orient --json | jq '.slots' # active, counted and cap
Related MCP tool
See also
- The daily loop.
status— projects only, without the commitments digest.
cdno status
A quick snapshot: your active projects and each one’s top next action. Lighter than
orient — no commitments digest, no suggestion.
cdno status [OPTIONS]
In a terminal this then offers to open one of the projects it just listed, printing what cdno project show would and asking again until you press Esc. Piped output, --no-interactive, and
--json skip the prompt. See Colour and interactivity.
Options
Only the global options. With --json, emits the snapshot as
structured data.
Examples
cdno status
cdno status --json | jq '.[].slug'
Related MCP tool
list_projects — the projects view for AI clients.
See also
orient— the fuller morning view.- Managing projects.
cdno now
What you are in the middle of: the action most recently started, unless it has since been completed, dropped or paused. Focus is one slot: starting something new displaces the old start, which never comes back.
cdno now [OPTIONS]
There is no state behind this. It replays the ## Logs of today and of the
[focus] carry_over_days days before it (default 1, so a start left open yesterday is still your
focus this morning), so a start made from
cdno action start, from an agent over MCP, or typed into the daily
note by hand all count — and a completion, a drop or a pause clears it. Nothing to keep in sync, and
nothing to go stale.
cdno action promote rewrites the bullet it matched, and the
focus follows it: the action promoted on line it logs renames the open start to the new note, with
the original start time, so action complete and action drop still close it.
A line you type yourself has to match the shape the writers emit:
- **09:30**: started [[surrogate-model]] — Draft the methods section (deep)
Both halves are required. The - **HH:MM**: stamp is what makes the line a log entry at all, and
the separator is an em dash (U+2014), not a hyphen. The parser requires that exact codepoint, so
that ordinary prose beginning “started something” is never mistaken for a focus. That strictness
means a line missing the stamp, or typed with -, is simply not picked up here — but
cdno lint reports it, naming the line and the likely cause, so a near-miss does not stay
invisible.
$ cdno now
On surrogate-model — Draft the methods section (deep), since 09:30 (1h 30m).
cdno now has three shapes.
Something is in focus. The line above. A start carried over from an earlier day names it
(since Saturday 14:05 (21h 7m)), and the elapsed time counts across midnight.
A carried focus that was picked up again. A focus carried over from an earlier day and
re-anchored with action resume reads the new time first and keeps the original:
$ cdno now
On surrogate-model — Draft the methods section (deep), picked up 08:50 today (started Saturday 14:05).
Resuming a pause shows the plain since HH:MM shape, because the pause had already closed the
earlier start.
A day more than six days back is shown as a date (2026-09-22 14:05), since a weekday name would be
ambiguous.
Nothing in focus. It says so, and shows the most recent pause that nothing has picked up since, with its re-entry hint:
$ cdno now
Nothing started.
Last paused: surrogate-model — Draft the methods section (10:40), next: pick up at "Prior approaches"
The pause is looked for in the last paused_lookback_days days
(default 14), so a Friday pause is still there on Monday. Pick it up with
cdno action resume.
--line
--line prints one sanitised line of at most 160 characters for a prompt segment or hook:
Focus: surrogate-model — Draft the methods section (since 09:30)
Focus: none (last paused: surrogate-model — Draft the methods section, next: pick up at "Prior approaches")
Control characters are stripped and an over-long line is cut. It prints nothing and exits 0 when no
vault is found or anything goes wrong, so it can sit in a prompt without ever breaking it. With
--json as well, --line wins. The Claude Code hook example
runs it before every prompt.
--json
| Key | Meaning |
|---|---|
project, action | The focus’s project slug and its bullet text exactly as logged (energy suffix and all). |
title | The bullet text without link and energy suffix, for display. For an attached note it is the last segment of the note’s slug. |
note | The attached action note’s path (actions/<slug>), or null for an inline bullet. |
energy | deep, medium or light. |
started | HH:MM of the open marker. Kept for compatibility. |
started_at, date | The marker’s full datetime (2026-10-04T09:30) and its day. |
carried | true when date is not today. |
origin | {started_at} of the earlier start a resume continues, else null. |
elapsed_minutes | Minutes since started_at, across midnight. |
last_paused | The most recent unresolved pause: project, action, title, at, next, reason. Present whether or not something is in focus; null when there is none. |
Every key is always there and is null when it has no value (all of them but last_paused when
nothing is in focus), so a caller can test one field without first branching on the shape of the
document.
The action field is the bullet text exactly as logged, energy suffix and all. That is the same
string cdno action complete matches, which is why the pairing
works.
Related MCP tools
current_focus — the same read, for an agent.
See also
- Focus — the idea behind the slot and the window.
action start— what puts something here;pause,switchandresumemove it.orient— what to begin when nothing is open.
cdno open
Open a note in your editor. Where cdno search answers “where did I write about
this”, open answers “take me to the note I mean” — it is addressing, not searching.
cdno open [OPTIONS] [REFERENCE]
Run it with no reference and it offers a picker over every note, most-recently-edited first.
Arguments
| Argument | Description |
|---|---|
[REFERENCE] | The note to resolve. Omit it and pass --list to see every note. Tab-completion offers your vault’s slugs. |
A reference can be any of:
| Form | Example | Resolves to |
|---|---|---|
| Bare slug | surrogate-model | the note with that slug, whatever its type |
| Type-scoped slug | project:surrogate-model | that slug within one note type |
| Calendar word | today, yesterday, tomorrow | the daily note for that day |
| Date | 2026-08-21 | that day’s daily note |
| ISO week | 2026-W34 | that week’s weekly note |
| Month | 2026-08 | that month’s monthly note |
| Vault-relative path | journal/2026/daily/2026-08-21.md | that file |
Path without .md, as a wikilink spells it | concepts/foo | concepts/foo.md |
| Absolute path inside the vault | /home/you/vault/projects/foo.md | that file |
Portfolios and expanded stewardships are addressed by their folder name, not the literal
_index: cdno open surrogate-model reaches portfolios/surrogate-model/_index.md.
The calendar words always mean the journal. A note genuinely named today stays reachable as
<type>:today.
Options
| Flag | Description |
|---|---|
--path | Print the note’s absolute path instead of opening it. |
--list | Print every note as path<TAB>title<TAB>type, for piping to a fuzzy finder. Opens nothing and takes no reference. |
--editor <COMMAND> | Editor command template for this invocation. Outranks $CUADERNO_EDITOR, $VISUAL, and $EDITOR. |
Plus the global options. With --json, a resolved reference emits
{"path": …} and --list emits an array of {path, title, type}.
When stdout is not a terminal, cdno open prints the path instead of launching anything. So
cdno open today | cat, a script, --no-interactive, and --json all behave the same way, and no
editor is ever started into a pipe.
Choosing the editor
First one set wins:
--editor <COMMAND> | this invocation only |
$CUADERNO_EDITOR | your shell |
$VISUAL, then $EDITOR | your shell |
| (nothing set) | the operating system’s default handler for .md |
export CUADERNO_EDITOR='code -g {path}'
{path} marks where the note’s path goes. Leave it out and the path is appended, so a bare
nvim works. Quoting is honoured, so a program name may contain spaces:
export CUADERNO_EDITOR='"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" -w {path}'
A value whose first word contains :// is handed to the operating system instead of executed,
with the path percent-encoded — obsidian://open?path={path}.
cdno waits for the editor to exit. It does not try to guess whether your editor is a terminal
or a GUI one — code -w blocks and code does not, and a guess would be wrong in a way you
could not override. Waiting is correct for every terminal editor and harmless for a GUI one that
returns immediately. If the editor exits non-zero, cdno open exits with the same code, the way
git commit treats an abandoned edit.
There is no per-vault editor setting, on purpose
You might expect .cuaderno/config.toml to carry this — a research vault that opens in Obsidian,
a code vault that opens in your editor. It deliberately does not.
A vault is a git repository, and --vault exists so cdno can be pointed at one you did not
create. A setting that names a program to run cannot live in data that gets cloned and synced:
opening a note in someone else’s vault would run their choice of program on your machine. It is
the same reason git does not honour every setting from a repository you just cloned.
Restricting such a setting to a bare binary name would not help. sh is a binary name, and
sh <the note> executes the note’s own contents — which, in a vault you cloned, the author also
wrote. The fix is that the setting comes from your shell rather than from the data.
If you work across vaults that want different editors, set CUADERNO_EDITOR in a per-directory
shell hook (direnv, or your shell’s chpwd), which keeps the decision on your machine.
Archived notes
Opening a note under actions/_done/ prints a warning first: its text was frozen when it was
archived, and cdno lint reports an edit to the existing text as an error. Appending
to it is fine. The warning does not stop you — markdown remains the source of truth.
When a reference does not resolve
Two cases, and they say different things:
- Nothing matched. The error names the closest few notes by their type-scoped form — so a mistyped slug tells you what you probably meant rather than dumping the vault.
- The slug matched more than one note.
cdno opennever guesses, because opening the wrong note is a mistake you only discover after typing into it. The error names each type-scoped form instead, and any of them resolves. This happens when a stewardship and a portfolio share a name, for instance —stewardships/gym.mdandportfolios/gym/_index.mdare bothgym.
A reference that looks like a path never falls back to fuzzy matching: a typo there means “no such file”, not a near-miss opened on your behalf.
Examples
cdno open # pick from every note
cdno open today # today's daily note
cdno open surrogate-model # by slug
cdno open project:surrogate-model # when one slug is used by two types
cdno open 2026-W34 # that week's weekly note
cdno open --path today # print the path, open nothing
cdno open --list | head # every note, tab-separated
cdno open today --json | jq -r .path
cdno open notes --editor 'code -g {path}'
With a fuzzy finder
cdno open has its own picker, so you do not need fzf at all. If you would rather use yours —
your keybindings, your preview window — --list gives it candidates. Because --list emits
vault-relative paths and open accepts absolute ones, the round-trip works from any directory,
with no need to cd into the vault first:
cdno open "$(cdno open --list | fzf --with-nth=2.. --delimiter='\t' | cut -f1)"
Worth a shell function:
cdo() {
local pick
pick=$(cdno open --list | fzf --with-nth=2.. --delimiter='\t' | cut -f1) || return
[ -n "$pick" ] && cdno open "$pick"
}
This beats running fzf over the vault directory directly, because the candidates carry each
note’s title rather than only its filename — filenames here are slugs, so a note titled
“Surrogate model” would otherwise never match the words you remember. The listing also respects
the vault’s ignore globs.
See also
search— full-text search when you do not know which note you want.- Searching your vault.
cdno weekly
Show the weekly review/plan note: Wins, Challenges, One Improvement, and This Week’s Goal.
cdno weekly [OPTIONS]
Options
| Flag | Description |
|---|---|
--date <DATE> | Any day in the target ISO week (YYYY-MM-DD). Defaults to this week. |
Plus the global options. weekly ignores --json.
Examples
cdno weekly # this ISO week
cdno weekly --date 2026-04-20 # the week containing 20 Apr 2026
To write the weekly sections rather than just view them, use the guided
review weekly.
Related MCP tools
read_weekly_note and get_weekly_context.
See also
cdno monthly
Show the monthly review note: Wins, Themes, Next Month’s Focus, and the month’s linked weeks.
cdno monthly [OPTIONS]
Options
| Flag | Description |
|---|---|
--date <DATE> | Any day in the target calendar month (YYYY-MM-DD). Defaults to this month. |
Plus the global options. monthly ignores --json.
Examples
cdno monthly # this calendar month
cdno monthly --date 2026-04-20 # the month containing 20 Apr 2026
To write the monthly sections rather than just view them, use the guided
review monthly.
Related MCP tools
read_monthly_note and get_monthly_context.
See also
cdno commitments
List aggregated commitments across the vault — the hard milestones of active projects, standalone commitment notes, stewardship periodic commitments, and self-imposed action-note deadlines — sorted by date, with overdue items flagged.
cdno commitments [OPTIONS]
Options
| Flag | Description |
|---|---|
--weeks <WEEKS> | Lookahead window, in weeks. Default 2. A standing 30-day overdue look-back always applies on top. |
Plus the global options. With --json, emits the list as structured
data.
Examples
cdno commitments # next 2 weeks + anything overdue
cdno commitments --weeks 6 # next 6 weeks
cdno commitments --json | jq '.[] | select(.overdue)'
How it’s computed
This is a derived view, not a single file. It merges four sources (see
Business rules):
project milestones marked --hard, stewardship periodic commitments, standalone
commit notes, and action notes with a self-imposed due:. A project’s own dates count while the project is open, active or parked: a hard date on a parked map is still a promise. Completing or dropping a project takes its milestones and action due dates out of the view until it is activated again. A standalone commitment that names a parked or closed project keeps surfacing, since it is a promise to someone else.
Related MCP tool
See also
- Commitments and deadlines.
commit— create a standalone commitment.
cdno questions
List active questions grouped by domain (research, life) — the orientation surface against the
question system. For lifecycle changes (park / answer / retire / activate), use
cdno question.
cdno questions [OPTIONS]
Options
Only the global options. With --json, emits the list as structured
data.
Examples
cdno questions
cdno questions --json | jq '.[].slug'
Related MCP tool
get_active_questions — which additionally accepts a domain filter.
See also
- Research and evidence.
question— create questions and transition their status.
cdno search
Full-text search across all notes, ranked best-first, with optional filters by note type, date window, and portfolio.
Titles are searched too, and weighted ten times a body match. A note whose title contains
your words ranks above one that merely mentions them, so you do not need a separate command to
look one up by name. When you already know which note you want and can name its slug or date,
cdno open resolves it directly instead of ranking.
cdno search [OPTIONS] <QUERY>
Arguments
| Argument | Description |
|---|---|
<QUERY> | Search text. Matched case-insensitively; terms are ANDed. Quotes and operators are treated as literal words. |
Options
| Flag | Description |
|---|---|
--type <TYPE> | Restrict to one note type (e.g. daily, project, evidence, or a config-defined custom type). A name that is neither built-in nor a registered custom type errors with the valid set; tab-completion offers your vault’s types. |
--from <FROM> | Inclusive earliest note date (YYYY-MM-DD). |
--to <TO> | Inclusive latest note date (YYYY-MM-DD). |
--portfolio <PORTFOLIO> | Restrict to notes in this portfolio. |
--limit <LIMIT> | Maximum results. Default 20. |
Plus the global options. With --json, emits an array of hits, each
with path, note_type, title, snippet, and score.
Examples
cdno search "preconditioner"
cdno search "sparse attention" --type evidence
cdno search "mesh" --from 2026-03-01 --to 2026-03-31 --limit 5
cdno search "speedup" --portfolio sparse-vs-dense-attention-ood --json | jq -r '.[].path'
Related MCP tool
See also
- Searching your vault.
reindex— rebuild the index if results look stale.
cdno review
Guided review rituals.
cdno review [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
weekly | Walk the retrospective sections into this week’s note and set next week’s goal. |
monthly | Walk the monthly sections into this month’s note. |
review ignores --json.
cdno review weekly
Walk the retrospective sections (Wins, Challenges, One Improvement) into this week’s note, then set next week’s goal as the This Week’s Goal of next week’s note. When run non-interactively, it reads the current note instead of prompting.
cdno review weekly [OPTIONS]
Options
Only the global options.
Examples
cdno review weekly # interactive: prompts for each section, sets next week's goal
cdno review weekly --no-interactive # read the current weekly note without prompting
cdno review monthly
Walk the monthly sections (Wins, Themes, Next Month’s Focus) into this month’s note. Unlike the weekly ritual there is no cross-note carry-forward — every section lands in the same month’s note. When run non-interactively, it reads the current note instead of prompting.
cdno review monthly [OPTIONS]
Options
Only the global options.
Examples
cdno review monthly # interactive: prompts for each section
cdno review monthly --no-interactive # read the current monthly note without prompting
Related MCP tools
get_weekly_context, read_weekly_note,
upsert_weekly_section, read_monthly_note,
upsert_monthly_section.
See also
- Weekly review.
- Note types.
weekly/monthly— just view the notes.
cdno project
Manage project maps: create, update state, set the core question, hand off and take back,
add/complete/drop milestones, manage waiting-on items, park/activate, complete/drop the project itself, and list/show. Next actions have their own verb, cdno action.
cdno project [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
create | Create a new project map |
state | Update the Current State (auto-logs the previous) |
core-question | Set or clear the core question (auto-logs the previous) |
hand-off | Name who else is moving a project (auto-logs the change) |
take-back | Make a handed-off project yours again (auto-logs the change) |
park | Move a project to _parked/ |
complete | Close a project whose work is done (moves it to _done/<year>/) |
drop | Close a project that is not going to happen (moves it to _done/<year>/) |
activate | Bring a parked or closed project back (enforces the cap) |
list | List active projects, or closed ones with --closed |
show | Show one project |
milestone | Add / complete / drop milestones |
waiting | Add / resolve waiting-on items |
Write subcommands honour --json (a {path, message} result, run non-interactively); list/show
emit their data under --json.
A parked project is a complete map, so every subcommand that writes a project’s body (state,
core-question, hand-off, take-back, milestone, waiting, and the
cdno action verbs that take --project, except start, switch and resume) accepts an active or a
parked slug. When --slug is omitted in a terminal, the picker and shell completion offer the active
and parked projects; park offers active projects only. See
Business rules.
cdno project create
Create a new project map. Created parked if you’re already at the active cap.
| Flag | Description |
|---|---|
--title <TITLE> | Project title (the slug derives from it). |
--slug <SLUG> | Name the new map’s file yourself instead of deriving it from the title. Not a selector: the other subcommands use --slug to pick an existing project. Refused if invalid or taken; see the slug rule. |
--context <CONTEXT> | Life domain: work, side-project, university, family, household, legal, personal. |
--question <QUESTION> | Vault-relative core-question wikilink target (e.g. questions/research/foo). Optional. |
--var <NAME=VALUE> | Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables. |
cdno project create --title "Surrogate model" --context work
cdno project create --title "Thesis" --context university --var ticket=ABC-123
cdno project create --title "Surrogate model for the mesh-scaling study" --context work --slug surrogate-model
cdno project create --title "Thesis" --context university --question questions/research/surrogate-cost
cdno project state
Update the Current State section. The previous state is auto-logged to today’s daily note first (see Business rules).
| Flag | Description |
|---|---|
--slug <SLUG> | Active or parked project slug. |
--text <TEXT> | The new state text. |
cdno project state --slug surrogate-model --text "Mesh scaling works; assembly is the bottleneck"
cdno project core-question
Set or clear the project’s core question after creation, auto-logging the previous value to today’s
daily note in the same was: / now: shape state uses.
core-question — --slug, and either --question <target> or --clear
cdno project core-question --slug surrogate-model --question questions/research/does-it-scale
cdno project core-question --slug surrogate-model --clear
--question takes the bare wikilink target, the same form
create --question takes — questions/research/foo, not [[…]], which is
rejected rather than double-wrapped.
Passing neither flag non-interactively is an error, not a silent detach: dropping a project’s question is a decision and has to be asked for.
cdno project hand-off
Record that someone else is moving an open (active or parked) project: the map gets
held_by: "<name>", and today’s daily note gets a holder on [[<slug>]] entry with indented was: /
now: lines, the side with no holder written (mine). A hand-off records a fact about the
project: someone else is moving it now, and the next move is theirs. Because the
active cap counts the projects that are
yours to move, an active project that names a holder is not counted; but handing off is never the
answer to a full cap. Name a holder only when someone else really has the project; to make room,
park, complete or drop one. The map stays where it is, and its deadlines stay in
cdno commitments.
| Flag | Description |
|---|---|
--slug <SLUG> | Active or parked project slug. |
--to <WHO> | Who has it: a name, or a wikilink target such as [[people/ana-ruiz]]. Prompted as “Who has it?”. |
cdno project hand-off --slug hull-survey --to "Ana Ruiz"
# Handed hull-survey to Ana Ruiz (projects/hull-survey.md)
A hand-off is refused when:
- the value names you or nobody (
me,tbd,someone, or another entry ofholder_sentinels): if you cannot name who has the project, it is yours. Keep it, or runcdno project take-backif it was handed off; - the value is a date, which names no one: give the person’s name instead;
- an action on the project is in focus: pause it
first with
cdno action pause, then hand it off; - the project is closed: only an active or parked project has a holder, so
activateit first if the work has come back.
Handing a project to the holder it already names changes nothing and logs nothing.
cdno project take-back
Clear a project’s holder: the map gets held_by: null and the daily note a holder on [[<slug>]]
entry with now: (mine). An active project then counts toward the cap again, so taking one back at
the cap is refused with the list of counted projects; park, complete or drop one of them first. A
parked project stays parked: taking it back clears the name only and needs no slot.
| Flag | Description |
|---|---|
--slug <SLUG> | Active or parked project slug. |
cdno project take-back --slug hull-survey
# Took hull-survey back (projects/hull-survey.md)
cdno project park
Move an active project to projects/_parked/, freeing a slot against the five-project cap.
| Flag | Description |
|---|---|
--slug <SLUG> | Active project slug. |
cdno project park --slug surrogate-model
cdno project complete
Close a project because its work is done. The map moves to projects/_done/<year>/<slug>.md with
status: completed and closed: set to today, and the daily log gets
project completed [[<slug>]] — <title>. It works on an active or a parked project and never needs a
slot, since closing adds nothing to the active count.
| Flag | Description |
|---|---|
--slug <SLUG> | Active or parked project slug. |
A completion is a claim that the work was done, so it is refused while any action or milestone is still open, and there is no flag to force it. The refusal lists what is open:
surrogate-model has 3 open items:
- [ ] Run feature set B on full geometry mesh (deep)
- [ ] [[actions/characterise-kan-ppo-sample-efficiency]] (deep)
- [ ] ICML paper submitted — hard: 2026-05-22
Still open, not touched: [[commitments/reviewer-report]] (due 2026-10-15)
Complete or drop each of them (or add it to the project that now owns it), then run again.
Tick what was done (cdno action done, milestone done), drop
what is not happening (cdno action drop, milestone drop,
each with its own reason), and run the command again:
cdno project complete --slug surrogate-model
# Completed surrogate-model. 4 of 5 slots in use.
Standalone commitments linked to the project are never touched: a promise to someone else does not end with the project. The success message names any that are still open.
cdno project drop
Close a project that is not going to happen. The map moves to projects/_done/<year>/<slug>.md with
status: dropped and closed: set to today, and the daily log gets
project dropped on [[<slug>]] — <title>, with the reason on an indented reason: line. Like
complete, it works on an active or a parked project and never needs a slot: ending a shelved plan
does not bring it back first.
| Flag | Description |
|---|---|
--slug <SLUG> | Active or parked project slug. |
--reason <REASON> | Why it is being dropped. Optional and never prompted for, but worth giving. |
--drop-open | Drop the project’s open actions and milestones with it, without asking. |
With open actions or milestones, drop lists them as complete does. In a terminal it then asks
Let these N go and drop <slug>? [y/N], and pressing Enter keeps everything. On y each open item
is dropped with the project, logged as its own action dropped on / milestone dropped on line
with reason: project dropped (<your reason>) (or plain reason: project dropped without
--reason), and an attached action note is archived as dropped.
If the list changed while you were answering, nothing is dropped and the new list is shown.
Non-interactively the list is printed and the command exits 1, unless you pass --drop-open.
cdno project drop --slug bayesian-opt-survey --reason "superseded by the ICML work"
cdno project drop --slug bayesian-opt-survey --reason "superseded" --drop-open
# Dropped bayesian-opt-survey. 4 of 5 slots in use. Let go: 2 actions, 1 milestone.
Items that were in fact done should be ticked first: a drop records that work was let go, not that it was finished.
Under --json a refusal from either verb prints the same project_has_open_items object the MCP
server returns (the items plus an open_items_hash) and exits 1; a success prints path,
message, dropped_actions, dropped_milestones and untouched_commitments.
A closed project can be closed again with the other outcome, as a new decision: a dropped project
that turns out to have been finished can be completed, and the reverse. It is re-filed under this
year’s folder with today’s closed: date and a new log line. Closing it again with the same outcome
is refused, naming the date it already has.
cdno project activate
Bring a parked, completed or dropped project back to projects/. A closed project also has its
closed: date cleared, so it can be closed again later with a fresh one. The log says
project [[<slug>]] activated. Fails if it would exceed the active cap — park another first. The
interactive picker and shell completion offer parked and closed projects, labelled.
| Flag | Description |
|---|---|
--slug <SLUG> | Parked or closed project slug. |
cdno project activate --slug surrogate-model
cdno project list
List active projects with a state snippet. Each project renders as a card — a coloured bar keyed to
its context, the slug as a title, and the state wrapped underneath. Honours --json.
A project that names someone else as its holder carries one more line, held by: <name> · does not count toward the cap. A value on holder_sentinels leaves the map
yours, so it prints no holder line. Under --json each row
carries held_by and counts (see JSON output).
In a terminal this then offers to open one of the projects it just listed, printing the same thing
cdno project show would and asking again until you press Esc. Piped output, --no-interactive, and
--json skip the prompt. See Colour and interactivity.
With --closed it lists completed and dropped projects instead, newest first, each with its outcome
and closing date. Under --json each row carries slug, title, context, outcome and
closed_on.
cdno project list
cdno project list --json | jq '.[].slug'
cdno project list --no-interactive # listing only, never a prompt
cdno project list --closed
cdno project show
Show a compact summary of a single project (any status). The slug is an optional positional: omit it
in a terminal and cdno offers a picker covering active and parked projects. Honours --json
(emits the project summary object).
For an active or parked project it ends with Held by: <name> when the map names someone else (a
sentinel prints none), and Counts toward the cap: yes or no.
cdno project show surrogate-model
cdno project show # pick from a list
cdno project show surrogate-model --json
cdno project milestone
Manage milestones — markers of progress. A --hard milestone is a real deadline counted in
cdno commitments.
add — --slug, --title, --date <YYYY-MM-DD> (optional), --hard
done — --slug, --query (case-insensitive substring of the milestone title)
drop — --slug, --query, --reason (optional)
cdno project milestone add --slug surrogate-model --title "Submit to ICML" --date 2026-01-22 --hard
cdno project milestone add --slug surrogate-model --title "All Round-1 replies received"
cdno project milestone done --slug surrogate-model --query "submit to icml"
cdno project milestone drop --slug surrogate-model --query "book the venue" --reason "the funder withdrew"
Use drop rather than done when the milestone is not going to happen — superseded, mis-typed, or
overtaken by events. done ticks the bullet and writes milestone done on ... to the daily log,
asserting a milestone that was met; drop removes the bullet and writes milestone dropped on ...
instead, so a later reader can tell a plan that changed from a plan that was kept. --reason is
optional and never prompted for: a correction is simply a drop with no reason. Only open - [ ]
bullets are matched — a completed milestone is a record of what happened, not a plan to revise.
--date is optional. Some milestones are gated by a condition rather than a date — “all Round-1
replies received” on a correspondence-driven project — and omitting the flag records the milestone
as target: TBD rather than making you invent an estimate. An undated milestone does not appear
in cdno commitments, which is the point: a date you made up reads later like a
commitment somebody made. It completes with done exactly like a dated one.
Interactively, the calendar is offered behind a yes/no so the undated case is reachable without knowing the flag can be omitted.
--hard requires --date: a hard deadline with no date is rejected rather than quietly downgraded
to a soft target.
cdno project waiting
Track external blockers.
add — --slug, --description
resolve — --slug, --query (substring of the item)
cdno project waiting add --slug surrogate-model --description "Cluster quota from IT"
cdno project waiting resolve --slug surrogate-model --query "cluster quota"
Related MCP tools
create_project, update_project_state,
park_project,
complete_project,
drop_project,
activate_project,
hand_off_project, list_projects,
get_project_context, add_milestone,
complete_milestone, set_core_question,
add_waiting_on,
resolve_waiting_on.
See also
- Managing projects.
action— the next-action list.
cdno action
Manage a project’s next actions: add (optionally as a manifest note), promote a bullet to a note, start, pause, switch, resume, complete, drop, and list.
cdno action [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
add | Append a next action to a project |
start | Log that work on an action is starting |
pause | Set the action in focus aside without finishing it |
switch | Pause the action in focus and start another, in one step |
resume | Pick a carried or paused action up again |
promote | Promote a plain bullet to a wikilinked manifest note |
complete | Mark an action done by substring match |
drop | Close an action without recording it as done |
list | List a project’s open actions |
Write subcommands honour --json ({path, message}, non-interactive); list emits its data under
--json. A refused start, pause, resume or switch prints a rejection object instead; see
When start is refused.
start, pause, switch and resume are the focus verbs: they move the one slot that
cdno now reads. See Focus for the idea
and the log markers they write below.
cdno action add
Append a next action to a project. --note also scaffolds a manifest note and wikilinks the bullet.
| Flag | Description |
|---|---|
--project <SLUG> | Active or parked project slug. |
--title <TITLE> | Action title. |
--energy <ENERGY> | deep, medium, or light. |
--note | Also create a manifest note alongside the bullet and wikilink it. |
--slug <SLUG> | Name the action note’s file yourself instead of deriving it from the title. Only with --note: a plain bullet has no file. Refused if invalid or taken; see the slug rule. |
--var <NAME=VALUE> | Value for a custom action-note template’s prompted variable ([variables.prompt]). Repeatable. Only applies with --note (a plain bullet isn’t templated). See Prompted variables. |
cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium
cdno action add --project surrogate-model --title "Characterise sample efficiency" --energy deep --note
cdno action add --project surrogate-model --title "Rerun the ablation on the held-out split with feature set B" --energy deep --note --slug feature-set-b-ablation
cdno action promote
Promote an existing plain bullet to a wikilinked manifest note. Substring-matches the bullet text; energy is inherited.
| Flag | Description |
|---|---|
--project <SLUG> | Active or parked project slug. |
--query <QUERY> | Case-insensitive substring of the bullet text. |
--slug <SLUG> | Name the new action note’s file yourself instead of deriving it from the bullet. Refused if invalid or taken; see the slug rule. |
--var <NAME=VALUE> | Value for a custom action-note template’s prompted variable ([variables.prompt]). Repeatable. Promotion scaffolds an action note, so it gathers the same prompts as add --note. See Prompted variables. |
cdno action promote --project surrogate-model --query "profile the assembly"
cdno action promote --project surrogate-model --query "profile the assembly" --slug profile-assembly
cdno action start
Log that work on an action is starting: writes started [[slug]] — <bullet> to today’s daily note,
which is what cdno now reads back.
The action must already be on the map. A start names a bullet, so that the later completion logs
matching text and the focus clears — a start naming nothing could never be closed. What gets logged
is the resolved bullet text, not your query, so --query "draft methods" logs
Draft methods (deep).
cdno action promote rewrites the bullet it matched, and the focus
follows it: cdno now switches to the new note ([[actions/<slug>]] (deep)), keeps the
original start time, and complete and drop still pair with the start.
| Flag | Description |
|---|---|
--project <SLUG> | Active project slug: starting work needs an active map. |
--query <QUERY> | Substring of an existing bullet. Conflicts with --unplanned. |
--unplanned | Start work that is on no map yet: adds the bullet, then starts it. |
--title <TEXT> | Title for the new bullet. Requires --unplanned. |
--energy <LEVEL> | deep, medium or light. Requires --unplanned. |
When start is refused
Focus is one slot, so start while another action is in focus is refused, naming the open one and
the way forward:
$ cdno action start --project other-project --query "CI"
Error: Draft the methods section is already in focus on surrogate-model (since 09:30).
To move on to this instead: cdno action switch --project other-project --query CI
Or complete or pause it first.
You can always move on; you just say so. In a terminal the refusal is followed by Switch to it instead? (default no); y runs the switch without asking for a hint or a reason. Piped, with
--no-interactive or with --json, it fails fast instead.
Starting the action that is the focus is refused too. If it was started today there is nothing to
do. If it was carried over from an earlier day the message points at
cdno action resume. A typo or an ambiguous --query is reported as
such, not as a refusal, and a refused --unplanned start adds no bullet.
With --json the refusal is the same object the MCP server returns, on stdout, with a non-zero exit:
{
"code": "focus_open",
"message": "An action is already in focus. Ask the person before switching; do not retry.",
"details": {
"focus": {"project": "surrogate-model", "action": "Draft the methods section (deep)",
"started": "09:30", "date": "2026-10-04", "carried": false},
"attempted": {"project": "other-project", "query": "CI"},
"same_action": false,
"remedy": "switch_action"
}
}
remedy is switch_action when something else is in focus, already_focused for the same action
started today, and resume_action for the same action carried over. attempted is
{project, query}, or {project, title} for an --unplanned start.
An ambiguous --query is a question rather than a dead end: in a terminal you get a picker over the
candidates, and non-interactively they are listed one per line. The same holds for
complete, drop and
promote, which resolve through the same matcher.
One case no picker can settle: two open bullets whose text differs only in case, or not at all. The domain’s whole-bullet tiebreak compares case-insensitively, so it sees two exact matches, declines, and picking either re-ambiguates. Edit one of the bullets to tell them apart.
# start something already planned
cdno action start --project surrogate-model --query "feature set B"
# start something that was never planned — adds the bullet and starts it
cdno action start --project surrogate-model --unplanned \
--title "Fix the CI badge" --energy light
--unplanned is deliberately explicit rather than a fallback when --query matches nothing: a
fallback would turn every typo into a new action, silently. --title and --energy require it, so
passing them alone is a parse error naming --unplanned rather than a start on some other bullet.
cdno action pause
Set the action in focus aside without finishing it. It acts on the current focus and takes no project or query: there is exactly one focus, and it is the one paused. Nothing is looked up on the project map, so a focus on a project you have since parked can still be paused, and the bullet stays where it is.
| Flag | Description |
|---|---|
--next <TEXT> | Where to pick up again: the re-entry hint resume and cdno now read back. In a terminal, asked for once when absent (Enter skips); no confirmation follows. |
--reason <TEXT> | Why the work was set aside. Never prompted for. |
cdno action pause --next 'pick up at "Prior approaches"' --reason "CI is red"
With nothing in focus it says Nothing started — nothing to pause. and exits non-zero. A pause is
not a drop: a paused action stays open on the map, and can be completed or dropped later as usual.
cdno action switch
Pause the action in focus and start another, in one commit. It takes the same target flags as
start, plus the hint and reason for the action being set aside.
| Flag | Description |
|---|---|
--project <SLUG> | Active project slug of the action to switch to: switching work needs an active map. |
--query <QUERY> | Substring of the open bullet to switch to. Conflicts with --unplanned. |
--unplanned | Switch to work on no map yet: adds the bullet, then starts it. |
--title <TEXT> | Title for the new bullet. Requires --unplanned. |
--energy <LEVEL> | deep, medium or light. Requires --unplanned. |
--next <TEXT> | Re-entry hint for the action being paused. Asked for once in a terminal when absent (Enter skips). |
--reason <TEXT> | Why the focus moved. Never prompted for. |
cdno action switch --project other-project --query "CI" \
--next "related-work paragraph half done" --reason "a collaborator is blocked on it"
A switch is a pause followed by a start in the log; there is no separate marker. If the target does
not resolve, nothing is written, not even the pause. With nothing in focus it is a plain start, and a
--next it had nothing to attach to is reported rather than dropped. A prompted switch shows pause <X>, start <Y> and asks to confirm, and the picker leaves out the bullet that is already in focus.
cdno action resume
Pick work up again. It takes no query or title: the text comes from the log, not from the map.
| Flag | Description |
|---|---|
--project <SLUG> | Restrict to this project: its carried focus, else its most recent pause. |
$ cdno action resume
Resumed on surrogate-model, logged to journal/2026/daily/2026-10-04.md
next: pick up at "Prior approaches"
Without --project it resumes the focus carried over from an earlier day if there is one, else the
most recent pause within paused_lookback_days (default 14). It writes
resumed [[slug]] — <text>, which re-anchors the focus to now, so a Tuesday start you keep working on
does not expire on Thursday, and prints the pause’s next: hint when it has one. A pause that a later
start, resume, completion, drop or promotion of the same action followed is no longer offered.
With nothing to pick up it says Nothing to resume — nothing is carried over or paused. (Nothing to resume on <project> — … with --project) and exits non-zero. It is refused with a focus_open refusal (the same object as above, without attempted) when a different action is in focus (pause it or switch
instead; resume never switches for you) or when the same action was already started today.
With --json a success carries path, message and resumed_from: kind (carried or
paused), date, next and reason.
What the focus verbs write
Each verb is one entry in today’s ## Logs. next: and reason: are indented continuation lines,
written only when given:
- **10:40**: action paused on [[surrogate-model]] — Draft the methods section (deep)
next: pick up at "Prior approaches"
reason: a collaborator is blocked on CI
- **10:40**: started [[other-project]] — Fix red CI on main (medium)
- **14:05**: resumed [[surrogate-model]] — Draft the methods section (deep)
paused closes the focus the way action done on and action dropped on do; resumed opens it again.
complete and drop are unchanged, including on a paused action. When a focus is open, a log line
written by cdno log, cdno capture or the triage verbs carries a
during: tag naming its project.
cdno action complete
Mark a next action completed by case-insensitive substring match. A wikilinked bullet also archives
its note to actions/_done/<year>/.
| Flag | Description |
|---|---|
--project <SLUG> | Active or parked project slug. |
--query <QUERY> | Substring of the bullet text. |
cdno action complete --project surrogate-model --query "feature set B"
cdno action drop
Close a next action without recording it as done — for work that was superseded, abandoned or
reprioritised. Matches the bullet exactly as complete does; a wikilinked bullet also archives its
note to actions/_done/<year>/, stamped status: dropped with no completion date.
| Flag | Description |
|---|---|
--project <SLUG> | Active or parked project slug. |
--query <QUERY> | Substring of the bullet text. |
--reason <TEXT> | Optional: why it was dropped. |
cdno action drop --project surrogate-model --query "demo proposal" \
--reason "superseded by the demo-planning action"
Use this rather than complete whenever the work was not actually performed. complete writes
action done on [[…]] into the daily log, which is the record the weekly review, the monthly scan
and every later verdict read back from — so completing something that never happened leaves the
vault asserting work nobody did, and the only repair is a correction line written by hand.
--reason is optional but worth giving: “superseded by X” and “no longer wanted” are different
facts, and only one of them tells a later reader to go looking for the replacement. It is recorded
on a continuation line under the log entry.
A dropped action does not appear in the completed-actions views that the weekly and monthly reviews build, because it carries no completion date.
cdno action list
List a project’s open action bullets, with attached-note status (active / blocked / completed / dropped) inline
when present. Honours --json.
| Flag | Description |
|---|---|
--project <SLUG> | Active or parked project slug. |
cdno action list --project surrogate-model
cdno action list --project surrogate-model --json
Related MCP tools
add_action, promote_action,
start_action, start_unplanned_action,
pause_action, switch_action,
switch_unplanned_action, resume_action,
complete_action, drop_action. (Open actions are also visible via
get_project_context; what is currently started via
current_focus.)
See also
cdno portfolio
Manage evidence portfolios: create, list, show, and link to a question or project. Filing evidence
into a portfolio is the separate cdno file verb.
cdno portfolio [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
create | Create a portfolio under portfolios/<slug>/ |
list | List portfolios with evidence counts + staleness |
show | Show a portfolio’s frontmatter + evidence inventory |
link | Link an existing portfolio to a question or project |
create/link honour --json ({path, message}); list/show emit their data under --json.
cdno portfolio create
Create a new portfolio. The slug derives from the question.
--question takes either free text or the slug of an existing question. Given a question’s slug, the
portfolio takes that question’s text as its title, the two are linked both ways, and the portfolio’s
slug is the question’s. An explicit --slug must then be omitted or equal to that slug; any other
value is refused, because the pair is correlated by slug.
| Flag | Description |
|---|---|
--question <QUESTION> | The question this dossier accumulates evidence for: its text, or the slug of an existing question. |
--slug <SLUG> | Name the portfolio’s folder yourself instead of deriving it from the question. Must match the question’s slug when --question names one. Refused if invalid or taken; see the slug rule. |
--project <PROJECT> | Optional wikilink to a project to associate it with. |
--var <NAME=VALUE> | Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables. |
cdno portfolio create --question "Sparse vs dense attention OOD"
cdno portfolio create --question "Sparse vs dense attention OOD" --project projects/surrogate-model
cdno portfolio create --question "Sparse vs dense attention OOD" --slug sparse-vs-dense
cdno portfolio create --question surrogate-generalises # from an existing question's slug
cdno portfolio list
List every portfolio with its evidence count and staleness. Honours --json.
In a terminal this then offers to open one of the portfolios it just listed, printing what cdno portfolio show would and asking again until you press Esc. Piped output, --no-interactive, and
--json skip the prompt. See Colour and interactivity.
cdno portfolio list
cdno portfolio list --json | jq '.[] | {slug, evidence_count}'
cdno portfolio show
Show a portfolio’s frontmatter and its evidence inventory. Honours --json (a detail object with the
evidence list).
| Flag | Description |
|---|---|
--portfolio <PORTFOLIO> | Portfolio slug. |
cdno portfolio show --portfolio sparse-vs-dense-attention-ood
cdno portfolio show --portfolio sparse-vs-dense-attention-ood --json
cdno portfolio link
Link an existing portfolio to an existing question or project (the retrofit path — pass exactly
one of --question/--project). Backlinks are set on both sides.
| Flag | Description |
|---|---|
--portfolio <PORTFOLIO> | Portfolio slug. |
--question <QUESTION> | Question to link (mutually exclusive with --project). |
--project <PROJECT> | Project wikilink to link (mutually exclusive with --question). |
cdno portfolio link --portfolio sparse-vs-dense-attention-ood --project projects/surrogate-model
cdno portfolio link --portfolio sparse-vs-dense-attention-ood --question questions/research/surrogate-cost
Related MCP tools
create_portfolio, get_portfolio_contents
(show), link_portfolio_to_question,
link_portfolio_to_project.
See also
- Research and evidence.
file— add evidence to a portfolio.
cdno file
File a piece of evidence into a portfolio. Without --attach it writes a plain Markdown evidence
note; with --attach it copies a non-Markdown artefact (PDF, image, video) into the portfolio and
scaffolds a linked evidence stub beside it.
cdno file [OPTIONS]
Options
| Flag | Description |
|---|---|
--portfolio <PORTFOLIO> | Portfolio slug. (Prompted/fuzzy-picked if omitted interactively.) |
--source <SOURCE> | Citation, experiment id, conversation reference, … |
--origin <ORIGIN> | Bare wikilink target to whatever produced this evidence (e.g. projects/foo); the CLI wraps it into [[...]]. |
--content <CONTENT> | Inline body. For a plain note it’s the content; with --attach it’s the abstract. Optional; defaults to empty. |
--attach <ATTACH> | Path to a non-Markdown artefact. Copied into portfolios/<slug>/<evidence-slug>/ with a stub that links to it. |
--move | With --attach, remove the source file after a successful copy (move instead of copy). |
--var <NAME=VALUE> | Value for a custom evidence template’s prompted variable ([variables.prompt]). Repeatable. Ignored with --attach (attachment stubs aren’t templated). See Prompted variables. |
Plus the global options. With --json, emits a {path, message}
result and runs non-interactively.
Examples
# A plain prose evidence note:
cdno file --portfolio sparse-vs-dense-attention-ood \
--source "Chen et al. 2025, NeurIPS" \
--origin projects/surrogate-model \
--content "4x speedup at 95% accuracy on the OOD split."
# Attach a PDF (copied into the portfolio; --content is the abstract):
cdno file --portfolio sparse-vs-dense-attention-ood \
--source "Chen et al. 2025" --origin projects/surrogate-model \
--attach ~/Downloads/chen2025.pdf --content "Key result: 4x speedup."
# Move the artefact in instead of copying:
cdno file --portfolio sparse-vs-dense-attention-ood --source "fig 3" \
--origin projects/surrogate-model --attach ./fig3.png --move
Evidence notes are append-only.
Related MCP tool
See also
- Research and evidence.
portfolio— create and inspect portfolios.
cdno question
Manage question notes: create one, then transition its status. Each status transition is logged to
today’s daily note. To list active questions, use cdno questions.
cdno question [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
create | Create a new question note |
park | Set status to parked |
answer | Set status to answered |
retire | Set status to retired |
activate | Set status to active |
All honour --json ({path, message}, non-interactive).
cdno question create
Create a new question under questions/<domain>/<slug>.md. The slug derives from the text.
| Flag | Description |
|---|---|
--domain <DOMAIN> | research or life. |
--text <TEXT> | The question text (becomes the body H1). |
--slug <SLUG> | Name the file yourself instead of deriving it from the text. Refused if invalid or taken; see the slug rule. |
--var <NAME=VALUE> | Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables. |
cdno question create --domain research --text "Does sparse attention beat dense OOD?"
cdno question create --domain research --text "Does the surrogate model generalise to unseen meshes?" --slug surrogate-generalises
Status transitions
park, answer, retire, and activate each take a --slug. The interactive picker offers only
eligible questions for that transition (e.g. you can only park an active question).
| Flag | Description |
|---|---|
--slug <SLUG> | Question slug. |
cdno question park --slug does-sparse-attention-beat-dense-ood
cdno question answer --slug does-sparse-attention-beat-dense-ood
cdno question retire --slug does-sparse-attention-beat-dense-ood
cdno question activate --slug does-sparse-attention-beat-dense-ood
Related MCP tools
create_question,
set_question_status. (List via
get_active_questions.)
See also
- Research and evidence.
questions— list active questions.
cdno stewardship
Manage stewardship dashboards: create (flat or expanded), list, show, and append a periodic
commitment line. Filing a tracking entry is the separate cdno track verb.
cdno stewardship [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
create | Create a stewardship (flat, or expanded with --tracking) |
list | List stewardships with variant, tracking count, staleness |
show | Show a stewardship’s frontmatter + dashboard excerpt |
add-periodic | Append a periodic commitment line |
complete-periodic | Complete one occurrence, rolling next: forward |
create/add-periodic/complete-periodic honour --json ({path, message}); list/show emit their data under
--json.
cdno stewardship create
Create a stewardship dashboard. --tracking makes it expanded (a stewardships/<slug>/ folder
with room for tracking/ and routines/); without it, the dashboard is a single flat file.
| Flag | Description |
|---|---|
--name <NAME> | Human-readable name (the slug derives from it). |
--slug <SLUG> | Name the dashboard’s file (flat) or folder (expanded) yourself instead of deriving it from the name. Refused if invalid or taken; see the slug rule. |
--context <CONTEXT> | Life domain (work, household, personal, …). |
--tracking | Create the expanded variant with a tracking/ folder. |
--var <NAME=VALUE> | Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables. |
cdno stewardship create --name "Finances" --context household # flat
cdno stewardship create --name "Health" --context personal --tracking # expanded
cdno stewardship create --name "Household finances and taxes" --context household --slug finances
cdno stewardship list
List every stewardship with its variant, tracking count, and staleness badge. Honours --json.
In a terminal this then offers to open one of the stewardships it just listed, printing what cdno stewardship show would and asking again until you press Esc. Piped output, --no-interactive, and
--json skip the prompt. See Colour and interactivity.
cdno stewardship list
cdno stewardship list --json | jq '.[] | {slug, variant}'
cdno stewardship show
Show a stewardship’s frontmatter and an excerpt of the dashboard body. Honours --json (a detail
object including variant and body_markdown).
| Flag | Description |
|---|---|
--slug <SLUG> | Stewardship slug. |
cdno stewardship show --slug health
cdno stewardship add-periodic
Append a periodic commitment line to the dashboard’s ## Periodic Commitments section. The line
becomes a row in the aggregated cdno commitments view.
| Flag | Description |
|---|---|
--stewardship <SLUG> | Stewardship slug. |
--title <TITLE> | Commitment title (e.g. “Dental check-up”). |
--every <RECURRENCE> | Recurrence: daily, weekly, monthly, yearly, or every N months. See Recurrence syntax. |
--next <YYYY-MM-DD> | Next due date. |
cdno stewardship add-periodic --stewardship health --title "Dental check-up" \
--every "every 6 months" --next 2026-09-01
cdno stewardship complete-periodic
Complete one occurrence of a periodic commitment, rolling its next: date forward by that line’s
own recurrence. Named to pair with add-periodic: it completes an occurrence, not the
stewardship, which is perpetual and never completes.
| Flag | Description |
|---|---|
--stewardship <SLUG> | Stewardship slug. |
--title <SUBSTRING> | Case-insensitive substring of the commitment’s title. |
--at <YYYY-MM-DD> | Date the work was actually done. Defaults to today. |
cdno stewardship complete-periodic --stewardship health --title "dental"
cdno stewardship complete-periodic --stewardship health --title "dental" --at 2026-08-25
Before this verb there was no way to mark a periodic commitment done: it is a bullet, not a note, so
cdno commit done has nothing to act on, and the reminder kept firing until the file
was hand-edited — the one edit the vault asks you not to make.
The next date is computed from the due date, never from --at. A check-up every 6 months, done
a week early each time, would creep a week earlier every cycle if the schedule followed the work.
Anchored to the due date, a run of early completions leaves the schedule where it was. A late
completion advances until next: is in the future, so one neglected commitment comes back on
schedule rather than several reminders deep.
One case the calendar imposes: a monthly commitment on the 31st has no 31st to land on in February, so it clamps to the 28th and keeps the 28th thereafter — the bullet records the next date, not the day the schedule nominally wants.
The entry records the completion and where the schedule moved to:
- **09:30**: periodic done on [[health]] — Dental check-up
was: 2026-09-01
now: 2027-03-01
A line whose recurrence the parser cannot read is refused rather than guessed — see
Recurrence syntax for the accepted forms. Such a line is otherwise unaffected:
it still appears in cdno commitments and lint still accepts it.
Related MCP tools
create_stewardship,
get_stewardship_tracking,
add_periodic_commitment,
complete_periodic.
See also
- Stewardships and tracking.
track— file a tracking entry.
cdno track
File a tracking note under an expanded stewardship. The activity is positional and selects the
template: a vault’s .cuaderno/templates/tracking-<activity>.md if present, else the generic
built-in. No activity-specific templates ship built-in — ready-made gym/body/swim variants are
in examples/templates/tracking/.
cdno track [OPTIONS] <ACTIVITY>
Arguments
| Argument | Description |
|---|---|
<ACTIVITY> | Activity slug — anything you track (e.g. gym, swim, reading). Selects tracking-<activity>.md if present, else the generic template. |
Options
| Flag | Description |
|---|---|
--stewardship <STEWARDSHIP> | Stewardship slug. Defaults to the only expanded stewardship if there’s exactly one; otherwise required. |
--routine <ROUTINE> | Bare slug of a routine doc — wrapped into [[stewardships/<slug>/routines/<routine>]] and substituted into the template’s routine: field. Only takes effect when the resolved template has a routine: field; the generic default has none, so it silently no-ops there. |
--content <CONTENT> | Inline body. Optional; defaults to empty so you can fill in tables afterward. |
--at <WHEN> | File the entry at a past (or near-future) day rather than today: YYYY-MM-DD, or a full YYYY-MM-DDTHH:MM[:SS] as cdno log --at takes (only the date is kept). For a session recorded after the fact — a statement reconciled days later, a reading taken this morning. Only the date is kept; the entry lands on that day and the daily-log line still goes into today’s note, naming the day it describes. Bounded to 50 years back and 1 year ahead: an unbounded date makes history writable, and a mistyped year would silently reshape a trend. |
--var <NAME=VALUE> | Value for a custom tracking template’s prompted variable ([variables.prompt]). Repeatable. Prompts come from the activity’s template (e.g. tracking-gym) when one exists. See Prompted variables. |
Plus the global options. With --json, emits a {path, message}
result and runs non-interactively.
Examples
cdno track gym --stewardship health --content "Upper body A; RDL up to 25kg"
cdno track body --stewardship health --content "Weight 78.4kg, resting HR 54"
cdno track swim --stewardship health --content "1500m, 28min"
# --routine needs a template with a routine: field (the example gym.md has one):
cdno track gym --routine upper-body-a
# With one expanded stewardship, --stewardship can be omitted:
cdno track gym --content "Legs day"
Tracking entries are append-only and only land in expanded
stewardships (those created with --tracking).
Until a vault has any tracking template, cdno track prints a one-line hint (on stderr) pointing
at examples/templates/tracking/
for a structured layout. It goes quiet once you author a template, and is suppressed under --json.
Merging a day
A second cdno track for the same activity and date merges into the first rather than
erroring: --content is appended to the entry’s ## Notes, and any metrics are folded into its
frontmatter. Recording a day in two passes — a morning and an evening session, spending logged as
it happens — is ordinary rather than exceptional.
Merging is not blind concatenation. A record carrying a stable id replaces the record with
that id, so re-applying the same payload is idempotent; a record without one appends, so
re-running an import that omits ids double-counts every summed metric. Scalars are
last-write-wins: there is no array to key on, and the later reading is what a level means — but
sending a plain value for a key that already holds records is refused, since that would discard
the day’s entries on a note type that only grows.
Related MCP tool
See also
cdno templates
Inspect and edit note templates. Use vars before writing a custom template in
.cuaderno/templates/ to see which {{placeholders}} a note type supports —
unknown placeholders render verbatim, so this is how you learn the valid set
without reading the source.
| Command | What it does |
|---|---|
vars <TYPE> | List the {{placeholders}} a type’s template supports. |
list | Every note type, which template is in effect, and where its override lives. |
show <TYPE> | Print a template’s effective content verbatim. |
eject <TYPE> | Copy a built-in template into .cuaderno/templates/ to customise. |
sync <TYPE> | Add frontmatter keys a release added to a built-in template to your customised copy, keeping your edits. |
save | Write a template, from a file, from stdin, or via $EDITOR. |
new | Scaffold a starter template for a config-defined custom type. |
cdno templates vars <type>
List the {{placeholders}} a note type’s template supports.
cdno templates vars [OPTIONS] <TYPE>
Arguments
| Argument | Description |
|---|---|
<TYPE> | Note type: project, action, question, portfolio, evidence, stewardship, tracking, commitment, daily, weekly, inbox, or a config-defined custom type. |
Takes only the global options.
Sources
Each placeholder is classified by where its value comes from:
| Source | Meaning |
|---|---|
supplied | Filled automatically by the note type’s create command. This is the type’s complete create-path key set — including body placeholders and keys the default template happens not to reference (e.g. daily’s weekday, tracking’s routine) — so it matches the per-type table in Customising templates and frontmatter exactly. |
config | A static [variables] entry in .cuaderno/config.toml, available to any template. |
prompt | A [variables.prompt] entry — a value must be provided at creation (via --var name=value, the MCP vars parameter, or interactively). The prompt message is shown. |
A config or prompt name that collides with a supplied key is omitted: the
supplied value shadows it, so it would never take effect.
With --json, emits an array of { name, source } objects (prompt entries
also carry message).
Examples
cdno templates vars project
cdno templates vars tracking
cdno templates vars question --json | jq -r '.[].name'
cdno templates eject <type>
Copy a built-in template into .cuaderno/templates/<type>.md as an editable
starting point. Note types use an in-binary default until you add a file for
them (only daily is seeded on cdno init); this materialises one so you can
customise it (add sections, reorder frontmatter, reference {{placeholders}}
from templates vars) without hand-reconstructing it from the source tree.
cdno templates eject [OPTIONS] <TYPE>
Arguments
| Argument | Description |
|---|---|
<TYPE> | Built-in note type to eject. Omit when using --all. A config-defined custom type has no built-in template to eject, so it is refused here — use templates new to scaffold one instead. Unlike templates vars, which does accept custom types. |
Options
| Flag | Description |
|---|---|
--all | Eject every built-in template into .cuaderno/templates/ at once. Types that already have a template file are skipped (a summary reports which), unless --force. Mutually exclusive with <TYPE> — pass one or the other. |
--force | Overwrite existing custom template(s). Without it, an existing file is left untouched (and single-type eject errors; --all skips it). |
Plus the global options. With --json, single-type
eject emits the { path, message } write result; --all emits an object with
written and skipped arrays (note-type names).
Only base note-type templates eject — no <type>-<variant> template ships
built-in. To create a tracking activity variant, copy one from
examples/templates/tracking/
to .cuaderno/templates/tracking-<activity>.md instead.
Examples
cdno templates eject project # → .cuaderno/templates/project.md
cdno templates eject tracking # → the generic tracking template
cdno templates eject project --force # overwrite an earlier customisation
cdno templates eject --all # eject all built-ins, skip customised
cdno templates eject --all --force # eject all, overwriting everything
The written file is exactly the built-in default, so a note created straight after ejecting is byte-identical to before — customise from there.
cdno templates sync <type>
Bring a customised template up to date with frontmatter keys a later release added to the built-in
one, without losing your customisation. A file you ejected or saved never receives such keys by
itself; nothing breaks without them (new notes from it just lack the key), but nothing else adds
them either, and eject --force would throw your edits away.
cdno templates sync [OPTIONS] <TYPE>
cdno templates sync --all [--check]
| Argument / flag | Description |
|---|---|
<TYPE> | Built-in note type whose override to sync. A config-defined custom type, including a bundled one such as concept, has no built-in to compare with and is refused: its template is kept up to date by hand. |
--all | Sync every built-in type that has a custom override. |
--check | Report what is missing and write nothing; exit non-zero if any override is behind. For CI and scripts. |
For each key of the built-in template’s frontmatter that your file lacks, sync inserts the built-in
line verbatim: a literal such as closed: null, or a placeholder line such as
core_question: {{core_question}}. It goes right after the nearest built-in key that precedes it
and your file has: after that key’s whole block, or, when that key has a one-line plain value,
straight after its line, so a comment above your next key stays with it; failing that,
right before the nearest built-in key that follows it; failing that, last in the frontmatter. A key
your file already has, quoted ("closed": null) or with a space before the colon, counts as
present. Nothing else changes: your own keys, their values, their order and the body stay
byte for byte, and no key is ever removed. Running it again on an up-to-date file writes nothing.
cdno templates sync project
# project: added `closed` to .cuaderno/templates/project.md; kept your `owner`
cdno templates sync --all --check # in CI: fails while any override is behind
--json emits an array with one object per override (with <TYPE> and no override, one
not_customised object), each with note_type, path, status (synced, behind, up_to_date,
not_customised, no_frontmatter or unreadable, the last with an error), added and kept
(your own keys, listed so you can see nothing was dropped). A file with no --- frontmatter block,
or one that cannot be read, is reported and left alone; an unreadable one makes the command exit
non-zero, with or without --check.
It writes through the same path as templates save.
cdno templates list and cdno lint both say when an override
has fallen behind.
cdno templates list
Every note type with the state of its template: whether a custom override exists, which source is in
effect, and the path the override lives (or would live) at. A custom override that lacks a key its
built-in has gained shows custom (lacks closed; …) in the Template column.
cdno templates list [OPTIONS]
--json reports source as a stable token — builtin_default, builtin_variant, custom_base,
custom_variant, or none — rather than the human label in the table, and lists the missing keys
under missing_builtin_keys (empty when there are none).
cdno templates show <type>
Print a template’s effective content verbatim: the custom override when one exists, else the
built-in default. A custom type with no template yet shows the starter new would write.
cdno templates show [OPTIONS] <TYPE>
| Option | Description |
|---|---|
--variant <NAME> | Show a <type>-<variant> template instead of the base one. Built-in types only. |
Output is byte-verbatim, so it round-trips:
cdno templates eject project
cdno templates show project | diff - .cuaderno/templates/project.md # no output
--json emits {content, source} instead.
cdno templates save
Write a template. On a built-in type this creates the custom override transparently — no prior
eject needed.
cdno templates save [OPTIONS]
| Option | Description |
|---|---|
--note-type <TYPE> | The type whose template to write. |
--variant <NAME> | Write the <type>-<variant> template. Built-in types only. |
--file <PATH> | Read the new content from this file, or from stdin when it is -. |
Without --file, an interactive run opens $EDITOR seeded with the template as it stands. Off a
terminal --file is required, so a scripted save cannot silently truncate a template to nothing.
cdno templates save --note-type project --file my-project.md
cat my-project.md | cdno templates save --note-type project --file -
cdno templates new
Scaffold a starter template for a config-defined custom type that has
none yet — a frontmatter block of type plus each declared required field as a {{placeholder}},
and a # {{title}} heading.
cdno templates new [OPTIONS]
| Option | Description |
|---|---|
--note-type <TYPE> | The custom type to scaffold for. |
Refuses a built-in type, which already has a default to edit — use eject or save for those — and
refuses to overwrite an existing template.
A custom type has exactly one template, so --variant is not accepted on show, save or new for
one; declare a separate note type instead.
Related
- Customising templates and frontmatter — how to write a custom template and use
[variables]/[variables.prompt].
cdno config
Inspect, check and edit .cuaderno/config.toml — the vault’s configuration file.
cdno config <COMMAND> [OPTIONS]
These verbs deliberately do not open the vault. Opening one validates the config first, so a config that will not parse means no vault at all — and that is exactly when you need to read, check and fix it. Every other command would fail with a bare “loading config.toml”; these report the line, the column and the reason.
Commands
| Command | What it does |
|---|---|
show | Print the config verbatim — comments, key order and spacing exactly as on disk. --json adds the content hash. |
validate | Run the exact check Vault::new performs. Exits non-zero on a bad config. --file <PATH> checks a candidate without putting it in place. |
edit | Open the config in $EDITOR, then save it through the validate-first, compare-and-swap gate. |
note-type set|remove | Add, change or remove a custom note type ([note_types.<name>]). |
note-type install | Install a note type that ships with cdno (such as concept) into this vault. See Installing a bundled note type. |
field set|remove | Add, change or remove a schema field ([schemas.<type>.fields.<field>]). |
plot set | Set how a declared tracking metric is plotted. |
var set|remove | Add, change or remove a static template variable ([variables]). |
prompt set|remove | Add, change or remove a prompted template variable ([variables.prompt]). |
The save gate
Every write — edit and all the structured setters alike — goes through the same three steps:
- Validate first. A config that would not reopen is never written.
- Compare and swap. If the file changed on disk since it was read, the save is refused rather than clobbering the other edit.
- Write verbatim. Comments and key order survive.
So a refused save always leaves the file byte-identical. edit additionally keeps your rejected
buffer and prints its path — a refused save must not also lose the work.
edit round-trips a scratch copy rather than opening config.toml itself: handing an editor the
real file would route the write around all three steps. It needs a terminal, and refuses an editor
that detaches instead of waiting, because there is then no moment at which the buffer is known to be
written.
Setting a value merges, it does not replace
note-type set and field set change only the flags you pass. An omitted flag keeps its current
value, so a one-flag edit cannot silently drop the rest of the entry.
To clear something, pass it empty (--template '', --required ''), or use the paired negative flag
for a boolean (--no-append-only, --no-settable, --no-log-on-change, --no-required).
Changing a field’s --type is the one exception: default and values are declared against the old
type, so they are dropped and the command says so. Re-set them in the same command or afterwards.
Installing a bundled note type
cdno ships with note types that cdno init declares in every new vault. Today there is one,
concept. note-type install adds one to a vault created
before it existed, so the older vault ends up exactly as a new one would:
cdno config note-type install --list # what ships, and whether this vault has it
cdno config note-type install --name concept --dry-run
cdno config note-type install --name concept
It takes three steps, in this order, and each writes only what is absent:
- Template. Creates
.cuaderno/templates/if needed and writes the type’s template, but only when no file of that name exists. A template you have edited is never overwritten; the report says whether it matches the bundled one. If your declaration names a different template file, the bundled one is not installed. - Folder. Creates the type’s folder (
concepts/) if absent. - Declaration. Appends the
[note_types.<name>]block, comment included, through the same save gate as every other verb. If the config already declares the type, the declaration is never modified: it is compared with the bundled one, key by key, and the report names thenote-type setcommand that would adopt each bundled value you might want.
declaration written
template written .cuaderno/templates/concept.md
folder created concepts/
Re-running is safe. When there is nothing left to write, the report ends with
concept: already installed, nothing to do and the command exits 0.
A config the vault would not open (check it with cdno config validate) is refused before anything
is written, even when the type is already declared.
Two refusals are phrased in plain words, and in both nothing is left behind:
- a config that declares
note_typesas an inline table cannot take an appended block; add it withcdno config edit; - another custom type already using the folder (
concepts) is refused by the gate.
--list prints each bundled type’s purpose, folder, fields, template and its section headings,
with this vault’s state: not installed, installed (matches), installed (declaration differs),
installed (template customised) or installed (template missing). --dry-run prints the exact
block and template that would be written, checks the result would validate, and writes nothing.
With --json, the report is one object with changed, as the other verbs carry it:
{"changed": true, "note_type": "concept",
"declaration": "written",
"template": {"path": "templates/concept.md", "action": "written"},
"folder": {"path": "concepts", "action": "created"}}
Once installed, a bundled type is an ordinary custom declaration: edit it with note-type set, or
delete it with note-type remove.
Options
Promptable arguments are flags. Omit one in a terminal and you are asked for it; omit one with
--no-interactive (or off a TTY) and you get a named error rather than a usage dump.
Common to every verb: the global options.
Examples
cdno config show # verbatim, pipeable back into the file
cdno config validate # exit 0 iff the vault would open
cdno config validate --file draft.toml # check a candidate first
cdno config edit # $EDITOR round trip through the gate
# Declare a custom note type, then extend it.
cdno config note-type set --name people --folder people --required name,email
cdno config note-type set --name people --folder humans # keeps required + template
cdno config field set --note-type people --field email --type string --required
# Add the concept library to a vault created before it existed.
cdno config note-type install --name concept
# Template variables.
cdno config var set --name author --value "Your Name"
cdno config prompt set --name subject --message "What is this note about?"
# Plot a declared tracking metric.
cdno config plot set --activity gym --metric weight --plot line
See also
- Configuration reference — every key the file accepts.
- Custom note types.
templates— the templates those note types render through.
cdno watch
Watch the vault and reconcile the index whenever a note changes outside cdno.
cdno watch [OPTIONS]
Every other command reconciles once, at startup, and exits. That is enough when the process is
short-lived, but it means a note you edit in another editor is invisible to search, lint and
backlinks until the next cdno command happens to run. watch closes that gap: it stays in the
foreground and reconciles on each debounced change.
Markdown on disk remains the source of truth — this only keeps the cache honest.
Options
Only the global options.
Behaviour
- Foreground until
Ctrl-C. There is no daemon mode; run it in a spare terminal or under your own process supervisor. - Debounced and coalesced. An editor’s atomic-save storm becomes one pass, and a bulk change
(a
git checkoutacross hundreds of notes) reconciles once rather than once per file. - Markdown only, outside
.cuaderno/. Reconciliation reads every directory in the vault, and the filesystem reports those reads — so reacting to them would make each pass trigger the next. Restricting to.mdfiles outside.cuaderno/keeps the watcher from hearing itself. - A failed pass does not stop the watch. The usual cause is a file caught half-written; the next event reconciles again.
Each pass prints what changed, so an edit that did not land is visible as no index change.
Limitations
- A
.cuaderno/config.tomlchange needs a restart. Honouring a newignoreglob means rebuilding the vault rather than reconciling it.watchsays so at startup rather than quietly using stale globs. - Interrupting a pass is safe but leaves the index incomplete until the next run — it is a cache, and the notes are untouched.
Examples
cdno watch # in the vault
cdno watch --vault ~/notebook # or point it at one
See also
reindex— force a full rebuild.- Business rules.
cdno frontmatter
Set typed frontmatter fields on a note through the index. Every other write
surface (cdno log, section upserts) is body-oriented; this is the one that
writes a frontmatter field, so a flag like the daily meds: true can be toggled
without a hand-edit that would desync .cuaderno/index.db.
The field must be declared in .cuaderno/config.toml under
[schemas.<type>.fields.<key>] and marked settable = true — the write is
driven entirely by that spec (see the configuration
reference).
cdno frontmatter set <note> <key> <value>
Set a declared, settable field to a new value.
cdno frontmatter set [OPTIONS] <NOTE> <KEY> <VALUE>
Arguments
| Argument | Description |
|---|---|
<NOTE> | The note to edit: today, a YYYY-MM-DD date (both resolve to the daily note), or a vault-relative note path (e.g. projects/foo.md). |
<KEY> | The frontmatter field to set. Must be declared settable = true under [schemas.<type>.fields.<key>]. |
<VALUE> | The new value, as a string. It is coerced to the field’s declared type (bool/int/float/string/date) and checked against any values allowed-set. |
Takes only the global options. With --json,
emits the { path, message } write result.
Rules
- Declared + settable, default-deny. An undeclared key is rejected; a
declared field without
settable = true(absent orfalse) is rejected. - Type-checked. A value that doesn’t parse as the declared type — or isn’t
one of a
stringfield’svalues— is rejected and nothing is written. - Reserved keys are blocked.
type,status, a calendar type’s period key (date/week/month), and a project’scloseddate are engine-owned regardless of config — use the lifecycle commands (cdno project park/activate/complete/drop,cdno question set-status, …) for those, so their auto-logging and index invariants are never bypassed. - No-op on no change. Setting a field to the value it already holds writes nothing and logs nothing.
- Optional auto-log. When the field declares
log_on_change = true, a real change stamps akey: old → newline into today’s daily note in the same commit. - Strict-exists (v1). The key must already be present in the note’s
frontmatter (the daily flags exist via the template default). A missing key
errors rather than being appended; ordered-insert is a planned follow-up.
Slug resolution for projects/questions is likewise a follow-up — v1 resolves
today/dates and explicit note paths.
Examples
cdno frontmatter set today meds true # toggle today's daily meds flag
cdno frontmatter set today workout true # if log_on_change, also logs it
cdno frontmatter set 2026-07-09 closed true # a specific day's daily note
cdno frontmatter set projects/surrogate.md phase review
Related
- Configuration reference — declaring
[schemas.<type>.fields], includingsettableandlog_on_change. - Frontmatter fields — the built-in per-type frontmatter shapes.
cdno note
Create, list and revise notes of a config-defined custom type (declared under
[note_types.<name>] in .cuaderno/config.toml). Built-in types have their own verbs
(cdno project create, cdno question create, …); this is the generic surface for custom types.
cdno note create <type>
Create a note of custom type <type>, written to <folder>/<slug(title)>.md.
cdno note create [OPTIONS] <TYPE> --title <TITLE>
Arguments
| Argument | Description |
|---|---|
<TYPE> | A config-defined custom type (e.g. person). A built-in type is refused — use its own create command. |
Options
| Flag | Description |
|---|---|
--title <TITLE> | Required. The note’s title; its slug becomes the filename. |
--slug <SLUG> | Name the file yourself instead of deriving it from the title. Refused if invalid or taken; see the slug rule. Not combinable with --field slug=…: naming the note twice is refused (slug_given_twice), since the two could disagree. |
--field <NAME=VALUE> | A frontmatter field, repeatable. Each key must be a declared required/optional field of the type; every required field must be supplied. |
--var <NAME=VALUE> | A value for the type’s template prompted variable, repeatable. |
--body-file <PATH> | The note’s body, read from a file, without the title heading (the engine writes the # <title> H1; a leading # <title> line in the file is dropped). Written verbatim apart from leading and trailing blank lines and trailing whitespace. It fills the template’s {{body}} placeholder, or is inserted after the H1 when the template has none. When the template has {{body}} and nothing else fills it (--field body=, --var body= or a [variables] body in the config), it is opened in an editor if omitted interactively, and required under --no-interactive. |
--origin <STRING> | Where the note came from: one string of wikilinks, e.g. [[journal/2026/daily/2026-09-02#Woodbury identity]]. Written, trimmed, as the origin frontmatter string even when the template omits or does not quote {{origin}}, so the type must declare origin (the concept type does); otherwise the command is refused. |
Plus the global options. With --json, emits a {path, message}
result. If the type ships no template (.cuaderno/templates/<type>.md), a minimal note is
synthesised from the declared fields plus a # <title> heading.
Creation is logged to today’s daily note as <type> created [[…]] — <title>; do not log it again
by hand.
cdno note list <type>
List every note of custom type <type>, by path.
cdno note list <TYPE>
cdno note revise [note]
Refine a mutable custom note (such as a concept) in place. The revision is logged to today’s daily
note as revised [[<path>]] — <reason>, or revised [[<path>#<Section>]] — <reason> for a
section, in the same write; do not log it again by hand. Built-in note types (project, action,
daily, …) and custom types declared append_only = true are refused.
cdno note revise [OPTIONS] [NOTE]
Arguments
| Argument | Description |
|---|---|
[NOTE] | The note to revise, resolved as cdno open resolves a reference: a vault path with or without .md, a slug, or type:slug. An unknown or ambiguous reference is reported as cdno open reports it. Omitted in an interactive session, a picker offers every note. |
Options
| Flag | Description |
|---|---|
--body-file <PATH> | Replace the whole body (everything after the frontmatter, which is kept as it is) with the file’s contents, written verbatim. Cannot be combined with --section. |
--section <STRING> | The heading text of a section to upsert, without the # markers. Requires --content-file. An existing section is replaced together with its sub-sections; a missing one is appended as ## <section>. |
--content-file <PATH> | The section’s new text, without its heading. Requires --section. |
--reason <STRING> | Why the note was revised, in a short clause; it becomes the daily-log line. |
Plus the global options. With neither --body-file nor --section,
an interactive session opens the note’s current body in your editor, then asks for the reason and
confirms before writing; under --no-interactive both are required. A note chosen in a picker
(an omitted, ambiguous or unmatched reference) also brings the confirm, even when every flag was
given. The note is read before
anything is gathered, and a note that changed on disk after that read (an edit saved while the
editor was open, say) is refused rather than overwritten: read it again and redo the revision.
Text identical to the note’s current content writes and logs nothing and prints
No change to <path>. That includes closing the editor without changing anything: the reason and
confirm are still asked for, and the result is No change to <path>. With --json, emits {path, message, changed, new_hash, log_line, section_target}.
cdno note rename [note]
Rename a note’s slug and follow every reference to it (RFC 0007): the wikilinks that resolve to it,
the project: / portfolio: style fields that name it, an aliases: entry for the old slug on the
note itself, and one renamed line in today’s daily log. The old name keeps resolving through the alias.
Renamable: projects, questions (a question whose slug a portfolio shares moves the portfolio with
it), active action notes, portfolios and custom notes kept as single files. A portfolio with a
same-slug question is renamed through the question. Dailies, weeklies, monthlies, evidence,
tracking, inbox items, archived action notes, stewardships, commitments, a custom note kept as a
folder and a note with no type are refused (note_not_renamable).
With --relink the note keeps its slug, and the links that
still reach it through a former name are rewritten to the current one. An archived action note can
be relinked, though not renamed.
Arguments
| Argument | Description |
|---|---|
[NOTE] | The note to rename, resolved as cdno open resolves a reference: a path, a slug, a former slug or type:slug. Omitted in an interactive session, a picker offers the renamable notes only; so does the picker an ambiguous or mistyped reference opens (a question and its same-slug portfolio resolve to the question). |
Options
| Flag | Description |
|---|---|
--to <SLUG> | The new slug. Prompted in an interactive session; required under --no-interactive (unless --relink). |
--relink | Keep the slug and rewrite every link that reaches the note only through a former name to it. Nothing moves. Cannot be combined with --to or --reason (a usage error, exit 2); composes with --dry-run and --skip-sync-check. See relinking the stragglers. |
--reason <STRING> | Why, in a short clause; it becomes the reason: line of the log entry. Never prompted. |
--dry-run | Print the preview, writing nothing. Refused exactly as the write would be (see the sync check); exits 0 only when the write would proceed. |
--skip-sync-check | Rename even when the vault is behind its upstream. A git operation paused mid-way still refuses. See the sync check. |
The preview
A long title makes a slug that stops mid-thought. This is a real run against a throwaway vault (see the tutorial for the whole story):
$ cdno project create --title "Calibrate the surrogate model against the high-fidelity thermal runs" --context work
Created projects/calibrate-the-surrogate-model-against-the.md
$ cdno action add --project calibrate-the-surrogate-model-against-the --title "Run the baseline sweep" --note --energy deep
Action added to projects/calibrate-the-surrogate-model-against-the.md with note actions/run-the-baseline-sweep.md
$ cdno action start --project calibrate-the-surrogate-model-against-the --query baseline
Started on calibrate-the-surrogate-model-against-the, logged to journal/2026/daily/2026-11-06.md
$ cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration --dry-run
Dry run: nothing written.
move projects/calibrate-the-surrogate-model-against-the.md -> projects/surrogate-calibration.md
rewrite actions/run-the-baseline-sweep.md 1 field (project:)
rewrite journal/2026/daily/2026-11-06.md 3 links
alias projects/surrogate-calibration.md gains `calibrate-the-surrogate-model-against-the`
log journal/2026/daily/2026-11-06.md
renamed [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
was: calibrate-the-surrogate-model-against-the
4 references in 2 notes · plan 2109…
$ cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration \
--reason "the derived slug stopped mid-title"
The last command prints the same preview, with the reason: line (the reason is part of the plan,
so its hash differs), and then Renamed `calibrate-the-surrogate-model-against-the` to `surrogate-calibration`. With the note and --to given on the command line the write runs straight
away, in a terminal or not: nothing is prompted, so nothing is confirmed. Use --dry-run first to
see the plan. Leave out --to in a terminal if you want to be asked for it, shown the preview and
asked to confirm.
A rewrite line reads N links, M fields (project:, …) or both (2 links / 1 field (project:)).
When the reference matched a former name, the first line is found by its former name `old` .
The log block is the entry as it will be appended, without its time stamp. Warnings follow the totals line. In an interactive session, anything that was prompted (the note, the
slug) brings the preview and a confirm. That confirmed write is given the preview’s plan hash within
the same command: it writes exactly what was shown, or, if a referrer changed while the confirm
waited, refuses with stale_plan and writes nothing. The hash does not join a --dry-run to a later
run, so a change made between the two is not caught: a dry run is a look, not a lock. A rename to the note’s current slug writes nothing.
With --json: the plan (target, from, new_slug, moves, edits with their changes, references,
notes, log_line, warnings, plan_hash) plus written (a bool), index_stale
and dry_run. Refusals are rejection objects and exit 1; a
rollback that itself failed (rename_incomplete) lists the paths to check by hand, one per line in text.
The sync check
A rename rewrites many notes at once, so in a vault that is a git repository synced with another
clone it first checks that it is not about to rewrite notes the other clone has already changed.
Unless --dry-run, it fetches the upstream (no credential prompt, at most 5 seconds; a fetch that
fails or times out prints one warning: line and the check reads the upstream as last fetched).
Then:
| The vault | The rename |
|---|---|
| is not a git repository | goes ahead; no check |
is a repository git cannot read (a timeout, a refused safe.directory) | warns and goes ahead |
| has a git operation paused mid-way (a conflicted merge, a rebase, a cherry-pick) | refuses with git_operation_in_progress; --skip-sync-check does not override it: finish or abort the operation first |
| is behind its upstream | refuses with behind_upstream, naming the remedy: sync first (git pull, or your sync), or pass --skip-sync-check |
| has no upstream, or it cannot be read | warns and goes ahead |
| has uncommitted changes | goes ahead: a dirty tree is never a reason to refuse |
The whole check runs again once the rename holds the vault’s write lock, with the same override
and no fetch, so a merge started, or a fetch landed, while the confirm prompt waits is still refused,
and nothing is written. A dry run is checked exactly as the write is, against the upstream as last
fetched (it never fetches): it refuses with the same code and exit code, so --dry-run --skip-sync-check is how to see the plan of a vault that is behind. Warnings go to stderr; under
--json they are warnings entries of kind sync_check in the plan instead (on stderr only before
a refusal).
Refusals
Every refusal writes nothing. With --json each is a rejection object
with the code below; in text the message is printed and the exit status is 1.
| Code | Why | What to do |
|---|---|---|
invalid_slug | --to breaks the slug rule (upper case, spaces, a leading -, a value YAML reads as a number or boolean, too long). | Pick a slug that follows the rule. It is refused, never rewritten. |
slug_taken | Another note, a folder note’s folder or another note’s former name already holds the slug; the message names the holder. | Pick another name. Nothing is suffixed. |
note_not_renamable | The note is of a kind the rename does not take (see which notes can be renamed). The reason is one of seven, below. | See below. |
stem_shared | Another note has the same file name, so a link to it cannot be told apart from a link to this one. | Rename or move the other note first (or fix the duplicate), then rename. |
reference_drift, reference_unaddressable | Rewriting a referrer’s frontmatter would change more than the reference, or a field names the note in a form the rewrite cannot address. The message names the note and the key. | Fix that note by hand, then run the rename again. |
stale_plan | A referrer changed while a confirm waited (any log append does it), so the plan you confirmed is no longer the plan. Over MCP the same code answers a write whose expected_plan no longer matches. | Preview again and confirm the new plan. |
behind_upstream | The vault is behind its upstream. The text is the vault is N commits behind its upstream; sync the vault first (pull the upstream's commits, e.g. with git pull), or pass –skip-sync-check to rename anyway. | Sync first, or pass --skip-sync-check. See the sync check. |
git_operation_in_progress | A merge, rebase or cherry-pick was left paused. Text: a git operation is in progress in the vault (…); finish or abort it, then rename. | Finish or abort it. --skip-sync-check does not override this. |
rename_incomplete | A write failed and the rollback could not restore every file; the paths are listed. | Check those paths by hand, then run the rename again. |
The seven note_not_renamable reasons, and what to do for each. In text mode the code itself is not
printed, only the message (note '…' cannot be renamed: <reason>); --json carries both.
reason | Meaning | What to do |
|---|---|---|
type <name> (type none for a note with no type:) | The kind never renames: daily, weekly, monthly, commitment, stewardship, evidence, tracking, inbox; or a note without a type. | Nothing: the note keeps its name. For a note with no type, add one first. |
archived | An archived action note, whose frozen prefix would change. A --relink is not refused: it changes nothing on the note. | Nothing: the note keeps its name. --relink still repairs the links to it. |
folder | A folder note other than a portfolio’s portfolios/<slug>/_index.md, of any type (a custom note kept as a folder, projects/<slug>/_index.md); or a portfolio kept as a single file. A portfolio on its own, in its folder, renames fine. | Nothing: the note keeps its name. |
rename the question | A portfolio whose slug a question shares (the portfolio alone is refused). A --relink of it is refused too, and relinking the question covers the question’s former names only; a rename never gives the pair’s portfolio aliases, so this matters only for one listed by hand. | Rename the question: the pair moves together. |
it has no frontmatter to record its old slug in | The note has no frontmatter block. | Repair the note (add frontmatter), then rename. |
its `aliases:` is not a list the old slug can be added to | aliases: is a scalar or some other shape. | Make aliases: a list, then rename. |
its `aliases:` lists the new slug in a form it cannot be removed from | The note takes back a former name that aliases: lists twice or with a tag. | Tidy the aliases: list by hand, then rename. |
A rename that would write over a file the vault already holds is refused as slug_taken. That
includes a rename that changes only the case of a slug (Alpha to alpha): it is refused where the
filesystem cannot tell the two names apart (macOS and Windows by default) and allowed where it can
(a default Linux filesystem). The message names the note itself as the holder (slug 'alpha' is taken by 'projects/Alpha.md' (as its stem)), which is expected. To change only the case, rename through a
temporary slug (Alpha to alpha-tmp, then alpha-tmp to alpha); the note ends up with both old
names as aliases. The old spelling (Alpha) stays listed but nothing resolves it, because aliases
must be slug-shaped; it can be deleted by hand.
Renaming back, and former names
The old slug is kept on the note as aliases: [<old>], and no other note may take it while it is
listed. Renaming old to new and then new back to old is allowed, and leaves aliases: [new].
In the log, the first renamed line’s head link is rewritten to [[old]], like any other link to the
note, while its was: old line stays as written: was: is a copied value, never a link, so it is the
record of what the note was called at the time. The second line reads renamed [[old]] — … was: new.
A command that is given a former name still finds the note. cdno open, cdno note revise and the
rename preview say so (found by its former name `old` ). cdno project show does not: it prints the
note’s current slug in its header, which is how to learn the new name there.
A link written with an old name still works in cdno, so leaving it is harmless. Editing it by hand to the new name is allowed too, even in a past log entry or an archived action note: it is the same address correction a rename makes.
Relinking the stragglers: --relink
A link written with a former name after the rename is a straggler: cdno still resolves it through
the alias, and cdno lint reports it, but an editor cannot follow it. The usual way one
appears is a vault synced between two machines with a journal merged line by line (merge=union):
the rename rewrote a log line on one machine, the other appended a line beside it, and the merge kept
both versions, so the old line comes back holding the old name (the merge keeps the whole hunk, so
usually several lines come back, not one). --relink rewrites every straggler in one go:
$ cdno lint
[warning] journal/2026/daily/2026-11-06.md: `[[calibrate-the-surrogate-model-against-the]]` names a renamed note — now `surrogate-calibration`
$ cdno note rename surrogate-calibration --relink --dry-run
Dry run: nothing written.
rewrite journal/2026/daily/2026-11-06.md 1 link
log journal/2026/daily/2026-11-06.md
relinked [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
references: 1
1 reference in 1 note · plan 1c09…
$ cdno note rename surrogate-calibration --relink
The last command prints the same preview and Relinked 1 reference to `surrogate-calibration`.
It goes through the same preview, plan hash, sync check and refusals as a rename, with these
differences:
- Only a reference through a former name is rewritten, links and fields alike. A link is rewritten when it resolves to the note through one of
its aliases (the links
cdno lintwarns about), and only to an address that resolves to the same note. A link that already names the note by its slug or path is left alone, and so is a link a former name no longer reaches (another note took that file name since). A reference can only ever move to where it already points. A field such asproject:that holds one of the note’s former names is rewritten too (lint does not report these, since a field is not a link): a field is read by its value alone, so until the relink it named no note at all. - It corrects addresses; it does not tidy the merge. The lines the union merge duplicated stay after the relink, now with identical text twice. They are the merge’s, not journal entries, so deleting the extra copy by hand is yours to do if you want it.
- An archived action note can be relinked. A rename refuses it (
archived, its frozen prefix would change), but a relink changes nothing on the target, so the links to a completed action that was renamed before it was archived can be repaired. Every other refusal of a rename applies:note_not_renamablefor the other reasons,stem_shared, the referrers’ own and the sync check. - Nothing moves and no alias changes. The note stays where it is, byte for byte unless it links
to itself by a former name, and its
aliases:stay listed, so an old link anywhere else (a fileignorehides, the other machine) keeps working. - The log line is
relinked. It readsrelinked [[<slug>]] — <title>with an indentedreferences: <n>line and nowas:, since the name did not change. With no straggler there is nothing to write:Nothing to relink: no link reaches `<slug>` through a former name., and nothing is logged.
With --json the shape is a rename’s, with moves empty and new_slug equal to from.
On the other machine
Some vaults live in a git repository with a working copy on more than one machine, synced by a tool that merges. Three things apply:
- Rename on a machine that is up to date. The sync check above refuses a rename while the vault is
behind its upstream. Your sync tool carries the result to the other machine on its next run. Use
--skip-sync-checkonly when you know the other machine has nothing unpushed that touches these notes, for instance when the upstream is unreachable and you accept the risk. - A conflict is the other machine’s edit meeting the rename. The sync stops rather than resolve
it. Resolve it as you would any merge, then run
cdno lint: a link the merge brought back with the old name is reported as a straggler ([[old]]names a renamed note — nownew). It still resolves in cdno, through the alias, but an editor cannot follow it: runcdno note rename <note> --relinkto rewrite every one to the current name (or fix one by hand). - Run any
cdnocommand in the vault once on every clone, on a cdno that stamps RLM 1.2 (one that hasnote rename), before renaming anything an archived action references. (Starting the MCP server on it counts too.) That records each archived action note in a form that ignores link addresses, which is howcdno linttells a corrected link from an edit. A clone that receives a renamed archived note before it has done so reports an archived action note error fromcdno lint(modified an existing line, ortruncated below its frozen prefixafter a shorter slug) that namescdno reindex.cdno reindexclears it, at the cost of the stored records: every archived action note then loses that check.
Examples
cdno note create person --title "Ada Lovelace" --field name=Ada --field role=advisor
cdno note create person --title "Ada Lovelace, programmer and mathematician" --field name=Ada --slug ada-lovelace
cdno note list person
cdno note rename calibrate-the-surrogate-model --to surrogate-calibration --dry-run
cdno note rename calibrate-the-surrogate-model --to surrogate-calibration --reason "the derived slug was too long"
cdno note rename calibrate-the-surrogate-model --to surrogate-calibration --dry-run --skip-sync-check # plan a vault that is behind
cdno note rename surrogate-calibration --relink --dry-run # the links still under a former name
cdno note rename surrogate-calibration --relink
cdno note revise concept:woodbury-identity --section Proof --content-file proof.md \
--reason "shorter proof via push-through"
Related
- Renaming a note — the long-title case end to end.
- Custom note types — declaring a type and the full feature.
- The concept library and
Building a concept library —
note create --originandnote revisein use.
cdno commit
Manage standalone commitments — dated promises kept as their own notes. To view all commitments
(from every source), use cdno commitments.
cdno commit [OPTIONS] <COMMAND>
Subcommands
| Subcommand | Description |
|---|---|
create | Create an active commitment at commitments/<slug>.md |
done | Mark a commitment completed and archive it |
drop | End a commitment that was not kept |
reschedule | Move an active commitment’s due date |
All four honour --json ({path, message}, non-interactive).
cdno commit create
Create an active commitment note. Optionally attribute it to a project or stewardship.
| Flag | Description |
|---|---|
--title <TITLE> | Commitment title (the slug derives from it). |
--slug <SLUG> | Name the commitment’s file yourself instead of deriving it from the title. Refused if invalid or taken; see the slug rule. |
--due <YYYY-MM-DD> | Deadline. |
--context <CONTEXT> | Life domain (work, personal, …). |
--project <SLUG> | Optional associated project. |
--stewardship <SLUG> | Optional associated stewardship. |
--var <NAME=VALUE> | Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables. |
cdno commit create --title "Pay rent" --due 2026-06-01 --context personal
cdno commit create --title "Review Erik's draft" --due 2026-05-20 --context work --project projects/icml-paper
cdno commit create --title "Send the surrogate-model results to the steering group" --due 2026-12-01 --context work --slug send-results
cdno commit done
Mark a commitment completed: stamps status and completed, and moves the note to
commitments/_done/<year>/<slug>.md.
| Flag | Description |
|---|---|
--slug <SLUG> | Commitment slug. |
cdno commit done --slug pay-rent
cdno commit drop
End an active commitment that was not kept — cancelled, superseded, or overtaken by events.
| Flag | Description |
|---|---|
--slug <SLUG> | Slug of the active commitment. |
--reason <TEXT> | Why it ended. Optional, and never prompted for. |
cdno commit drop --slug quarterly-report
cdno commit drop --slug quarterly-report --reason "the client cancelled the engagement"
Use this rather than done when the promise was not fulfilled. done writes
commitment completed [[slug]] into the daily log, which every weekly and monthly review reads back
from, so using it for a cancelled promise makes the vault assert something nobody did. Deleting the
note is not the alternative either: that destroys the record that the promise was ever made, and
desyncs the index on the way out.
The note is archived to commitments/_done/<year>/ like a completion, stamped status: dropped
with completed cleared — so it never appears as completed work. The log entry says what happened,
with the reason on its own line:
- **16:30**: commitment dropped on [[quarterly-report]] — Quarterly report
reason: the client cancelled the engagement
A dropped commitment can afterwards be neither completed nor rescheduled. Nothing is destroyed — the
note keeps its body and its created date — but a drop is re-decided rather than undone: if the
promise comes back, make it again, and the record shows both decisions.
cdno commit reschedule
Move an active commitment’s due date, recording the move in today’s daily note.
| Flag | Description |
|---|---|
--slug <SLUG> | Slug of the active commitment. |
--due <YYYY-MM-DD> | The new due date. Must differ from the current one. |
cdno commit reschedule --slug quarterly-report --due 2026-06-15
Commitments slip, and that is normal rather than an error state. Use this instead of deleting and
recreating the note: recreating destroys the body — the notes on who was chased and what was
promised — and resets created, the one field that shows how long something has been slipping.
The log entry carries both dates:
- **09:30**: commitment rescheduled on [[quarterly-report]] — Quarterly report
was: 2026-06-01
now: 2026-06-15
That is what makes repeated slippage visible instead of silently rewritten: a commitment moving once is a checkpoint, a commitment moving for the fourth time is a signal. Moving a date earlier is allowed; moving it to the date it already carries is refused, since the entry would assert a slip that never happened.
Related MCP tools
create_commitment, complete_commitment,
drop_commitment, reschedule_commitment. (View via
get_commitments.)
See also
- Commitments and deadlines.
commitments— the aggregated view.
cdno lint
Validate every indexed note and report what is wrong with it — frontmatter, links, attachment
pairing, and lines the canonical parsers silently skip. Errors fail the command; warnings (such as
broken wikilinks) are non-fatal unless --strict is given.
A wikilink or embed that points at an attachment — a pasted image, a filed PDF — is not a broken
link: attachments are never indexed, but the target is resolved against the filesystem (relative to
the linking note, then to the vault root) before a link is called broken. Only a target that matches
nothing at all is reported, and a missing ![[embed]] reads as a missing file rather than a link
that “resolves to no note”.
A link that resolves only through a note’s aliases: entry — a former name — is a straggler: it still
works, but lint warns and names the note’s current slug ([[old]] names a renamed note — now new),
so you can fix the link by hand, or rewrite them all with
cdno note rename <note> --relink. Lint also warns when an alias collides with another note’s name. If it equals another note’s file
stem, the stem wins and the alias can never be followed (alias x on <path> is shadowed by
<other>). If it equals the folder name of a folder note such as portfolios/x/_index.md, the alias
takes the bare link [[x]] (alias x on <path> takes bare [[x]] from folder note <other>);
the folder form [[portfolios/x]] still reaches the folder note. A straggler to a folder note is
told to use the folder form, since a bare link to the new name would not resolve.
It also reports lines that were plainly meant to be structured but will never be read as such,
because the parsers that consume them skip what they cannot parse rather than complaining. That
covers malformed ## Active Habits and ## Periodic Commitments bullets on a stewardship
dashboard, and — in a daily note’s ## Logs — a start or close marker that
cdno now will not see:
[warning] journal/2026/daily/2026-09-15.md: log line `- **09:30**: started [[alpha]] - Draft methods (deep)` reads as a `started` marker but `cdno now` will not see it -- found an ASCII hyphen (-) where an em-dash (—) separates the slug from the action
The shape has to be - **HH:MM**: started [[slug]] — text, and every part of it matters: the -
bullet and the stamp are what make the line a log entry at all, and the separator must be a real em
dash (U+2014). A stamp that was attempted and mangled — - 09:20: unbolded, - **25:99**: out of
range, - **09:40** with no colon — is caught too, not only one that is missing outright. The check is
deliberately narrow — the marker has to open the entry and be followed immediately by [[ — so
ordinary prose in ## Logs, including a sentence that merely mentions starting something or names a
note mid-sentence, is never flagged. The renamed [[new]] — Title line a rename writes, and the
relinked [[new]] — Title line of --relink, get the same near-miss check (neither is a focus
marker, so cdno now never reads them), and a was: continuation
under it that contains a [[ link is a warning, because the value is a copied slug and a link would
be rewritten by the next rename.
It checks that an archived action note (actions/_done/<year>/) has only grown since it was
archived. The archival recorded the note’s frozen prefix; lines appended after it (a late
retrospective) are fine, while an edit to an existing line, or a cut into the prefix, is an error:
[error] actions/_done/2026/characterise.md: archived action note modified an existing line (append-only after completion)
A rename may change the prefix in one way only: it re-targets references, rewriting the address of a
wikilink ([[old]] becomes [[surrogate-model]]) and a reference field such as project:. So the
prefix is accepted when either its bytes are unchanged or they match with every wikilink address and
reference-field value set aside. The cost of that rule is that a hand edit which only re-targets a
reference inside a frozen prefix is no longer detected; any other change still is. This holds on every
clone that opened the vault once on this version with the backfill completing (it is retried at every open, and a lock timeout only postpones it) before the renamed file arrived: a note archived on an
older version and renamed elsewhere first is reported as an error that names
cdno reindex, which clears it by rebuilding the index without any archival snapshots,
so every archived note loses this check. On a renamed note, a hand addition that follows an unmatched
backtick at the end of the prefix with no blank line in between is reported too; start a late addition
after a blank line. A note archived by
a newer cdno, whose snapshot this cdno cannot read in the second form, gets a warning naming the
version, and only the unchanged-bytes form is checked for it.
It checks each project map’s status against the folder the map sits in and against its closed:
date. A status its folder contradicts is an error, since the project verbs find a map by its folder
and act on it by its status; the message says which way to repair it:
[error] projects/_parked/alpha.md: project says `status: active` but is not at `projects/<slug>.md`; move it there, or set the status its folder stands for
A completed or dropped map outside projects/_done/<year>/ is told to move there and set
closed: <date>, or to set status: active and close it with
cdno project complete. A closed: date that disagrees with the status is a
warning: a completed or dropped map without one cannot be placed by the reviews, and an active or
parked map carrying one should have closed: null.
It reads each open map’s held_by: (the name of whoever else is moving it) against the vault’s
[projects] holder_sentinels list (see configuration). A value that is not a
plain name, such as an unquoted wikilink, is an error, because the holder you wrote is not honoured
and the map still counts toward the cap (or would, once a parked map is activated). For an unquoted
wikilink the message says to quote it; for anything else (a number, a date, a list) it says to write a
single quoted name:
[error] projects/harbour.md: project `held_by:` is not a plain name, so the holder is not honoured and the map counts toward the cap; quote it: `held_by: "[[people/ana]]"`
A value on the list, or a blank one, names you or nobody, so the map is yours and counts. That is a warning:
[warning] projects/harbour.md: project `held_by: "tbd"` does not name someone else: delete the line, or name the holder; a holder that names you or nobody means the map is yours
And it warns, once, on .cuaderno/config.toml when the projects that count (active and naming no
other holder) exceed max_active_projects, listing them. The project verbs refuse to go over the
cap, so this catches a cap lowered in config or maps edited by hand:
[warning] .cuaderno/config.toml: 6 projects count toward the cap of 5 (`max_active_projects`): [[alpha]], [[bravo]], [[charlie]], [[delta]], [[echo]], [[foxtrot]]; park or close one until no more than 5 count
If a project map cannot be read at all (an unknown status, a missing context or created), the
count is unknown, and the same file gets an error saying so: could not count the projects against the cap: …. The project verbs fail on such a map too, so fix it first.
It also warns when a customised template (.cuaderno/templates/<type>.md) lacks a frontmatter key that
the built-in template has, typically one a release added after you customised it, and reports a
customised template it cannot read (for instance one that is not UTF-8) as an error:
[warning] .cuaderno/templates/project.md: custom template `project` lacks `closed` (run `cdno templates sync project`)
cdno templates sync adds the key without touching your
edits. Lint cannot tell a new key from one you removed on purpose, so a built-in key you deleted from
your template is reported too.
It reads [vault] method_version (see configuration) and
reports, on .cuaderno/config.toml, a vault this cdno would not write to as an error: one on an
older major version (the message points at cdno migrate --dry-run, then cdno migrate) or a
newer major (upgrade cdno). A vault on a newer minor version is a warning, since writes
proceed:
[warning] .cuaderno/config.toml: this vault follows RLM 1.3, a newer minor version than the RLM 1.2 this cdno implements — writes proceed; upgrade cdno for the practices it adds
A vault on this cdno’s version, or on an older minor one (an unstamped vault included), gets no
row here. A malformed method_version ("v1", "1.1.0") is not a lint row: it is a
configuration error, so the vault does not open and every command, lint included, fails with an
error naming the key. The one exception is a long-running server (cdno-mcp-server) whose config
was edited to a malformed value after it opened: it keeps running, and there lint (and the MCP
lint tool) reports one error row on .cuaderno/config.toml naming the key instead of aborting.
Fix the value (see configuration).
While the vault records a method version below RLM 1.1 (an unstamped vault reads as 1.0), it gives
one warning per overdue hard date on a parked project map, on every run, since the commitments
register keeps those dates as of RLM 1.1: the one-time read-through of parked deadlines. The row sits on the map for a hard
milestone and on the action note for a due:, and names the fix:
[warning] projects/_parked/beta.md: hard milestone `Grant report` (2026-04-15) on parked project [[beta]] is overdue and in the commitments register again (a parked map's hard dates are promises under RLM 1.1): drop it with `cdno project milestone drop`, or re-date it on the map; this read-through row disappears once the vault records RLM 1.1, which `cdno migrate` records
An action note’s row says to re-date its due: in the note, or drop the action with
cdno action drop. Future dates are not warned on;
cdno migrate --dry-run lists them all. Once the vault records RLM 1.1 the rows are
gone for good.
cdno lint [OPTIONS]
Options
| Flag | Description |
|---|---|
--strict | Treat warnings as failures too (exit non-zero on any issue). |
Plus the global options. lint ignores --json.
Exit status
- Clean, or warnings only without
--strict→ exit0. - Any error (e.g. unknown note type, invalid frontmatter) → non-zero.
- With
--strict, any warning also → non-zero. Useful in CI to keep a vault pristine.
Examples
cdno lint # report issues; fail only on errors
cdno lint --strict # fail on warnings too (e.g. broken links)
Related MCP tool
lint.
See also
cdno reindex
Rebuild the SQLite index from scratch off the Markdown source of truth. The recovery path for a corrupt or stale index.
cdno reindex [OPTIONS]
Options
Only the global options. reindex ignores --json.
When you need it
Almost never — Cuaderno reconciles the index automatically on every run (see
Business rules). Reach for reindex when:
- search or link results look wrong after a large external edit or a sync conflict, or
- you deleted
.cuaderno/index.dband want to rebuild it eagerly rather than on next use.
Because the Markdown files are authoritative, a full rebuild loses no note. It does drop one thing
the Markdown cannot rebuild: the archival snapshots, so every archived action note loses the
frozen-prefix check cdno lint runs on it.
Examples
cdno reindex
See also
cdno migrate
Record the version of the Research Logbook Method this cdno implements in the vault
([vault] method_version in .cuaderno/config.toml), running any file migrations that version
needs and logging the move in today’s daily note.
cdno migrate [OPTIONS]
Options
| Flag | Meaning |
|---|---|
--dry-run | Print what would change, and write nothing. |
Plus the global options. --json prints the same report as an
object: dry_run, outcome (planned, migrated or nothing_to_do), vault (null when
unstamped), vault_described, binary, migrations, config_line, log_entry and
read_through.
migrate asks nothing and confirms nothing: run it with --dry-run first to see exactly what
will change.
What it does
-
This cdno implements RLM 1.2. A vault without a recorded version is unstamped and read as RLM 1.0;
cdno migratestamps it, or a vault stamped1.0, with1.2. -
File migrations, when a version needs any, never touch the journal or any append-only note. When there are none the report says
none: additive. -
The daily log gains one entry:
vault migrated was: unstamped (read as 1.0) now: 1.2 -
A vault below RLM 1.1 gets a read-through, whether or not it is stamped: every hard date on a parked map (a hard milestone, or an action note’s
due:), which the commitments register now keeps, listed with its map, overdue first. Drop or re-date each;cdno lintwarns on the overdue ones until the vault records RLM 1.1.read-through (hard dates on parked maps, overdue first; drop or re-date each): 2026-04-15 Grant report (hard milestone on [[beta]], overdue) 2026-05-01 Shelved draft (action due on [[beta]], overdue) 2026-11-01 Final review (hard milestone on [[beta]]) -
A vault already at this cdno’s version prints
vault is at RLM 1.2; nothing to do: its read-through is done. -
A vault on a newer version than this cdno is refused with a non-zero exit: upgrade cdno instead.
migrate runs only when you run it: never when a vault opens, and never over MCP.
The notice
When a vault records an older method version than this cdno, other commands print one line on
stderr after they finish, naming both versions and pointing at cdno migrate --dry-run. A vault
on a newer minor version gets a line saying writes proceed and cdno can be upgraded. migrate,
init, now --line and shell completion never print it, and stdout (including --json) is
never touched.
Examples
cdno migrate --dry-run
cdno migrate
See also
init,lint.- Configuration: method version.
- Troubleshooting: writes are refused with a method version error.
cdno normalise
Reorder note frontmatter into the canonical key order of each note’s template (a custom
.cuaderno/templates/ override if present, else the built-in). Notes cdno creates are already
canonical; this fixes hand-authored or migrated notes.
cdno normalise [OPTIONS]
Options
| Flag | Description |
|---|---|
--check | Report out-of-order notes without rewriting them. Exits non-zero if any are out of order. |
Plus the global options. normalise ignores --json.
Examples
cdno normalise # rewrite notes into canonical frontmatter order
cdno normalise --check # report only; non-zero exit if anything is out of order (CI-friendly)
normalise only reorders existing keys — it never changes values or adds/removes fields. The
canonical order is whatever the matching template
defines.
See also
- Configuration — how templates define field order.
lint.
cdno completions
Print a shell-completion script. Source it in your shell’s rc file. The script wires vault-aware
dynamic suggestions — --project, --portfolio, --stewardship, --slug, etc. are completed by
re-invoking the binary when you press TAB.
cdno completions [OPTIONS] <SHELL>
Arguments
| Argument | Description |
|---|---|
<SHELL> | Target shell. One of bash, zsh, fish, elvish, powershell. |
Options
Only the global options.
Setup per shell
# zsh — in ~/.zshrc:
source <(cdno completions zsh)
# bash — in ~/.bashrc:
source <(cdno completions bash)
# fish — write it into the completions dir:
cdno completions fish > ~/.config/fish/completions/cdno.fish
# elvish / powershell: emit the script and source it per that shell's convention.
cdno completions powershell
After reloading your shell, TAB completes commands, flags, and live vault values (project slugs, portfolio slugs, …).
See also
MCP server reference
cdno-mcp is a Model Context Protocol server that exposes your
vault to AI clients (Claude Desktop, Claude Code, Kiro, Gemini CLI, …). It runs the same domain
engine as the CLI, so anything the assistant does goes through the same rules and lands in the same
Markdown files. To wire it up, see Connect to Claude.
Transport and vault selection
Two binaries serve the same tool catalogue:
cdno-mcpspeaks JSON-RPC over stdio — the client launches it as a subprocess. It opens the vault named byCUADERNO_VAULT_PATH, or, if that’s unset, discovers one from its working directory (the same rule as the CLI).cdno-mcp-serverspeaks MCP Streamable HTTP for remote clients — see The HTTP server for its flags and security model.
The tool surface
The server advertises 66 tools. This reference groups them by purpose:
| Group | Page | What’s in it |
|---|---|---|
| Context-gathering reads | Context-gathering tools | Orientation, project/portfolio/weekly context, search, reads, lint, triage list |
| Writes | Write tools | Log, capture, file evidence, project/action/milestone/waiting edits, note revision and renaming, commitments, tracking, daily/weekly sections |
| Creation & lifecycle | Creation and lifecycle tools | Create projects/portfolios/questions/stewardships, link portfolios, park/activate/complete/drop, status transitions |
Every tool returns typed JSON; the shapes mirror the CLI’s --json output, so a
client gets the same structures whichever surface it uses.
Focus
Four of the write tools (pause_action, switch_action, switch_unplanned_action, resume_action)
move the one “what I am on” slot that current_focus reads, and eighteen write results carry a
focus field so the focus rides along without a second call. The server never refuses a write for
being outside the focus; it refuses a second start (or a switch to the focused action, or a resume_action over a
different focus) with a coded focus_open rejection, and pause_action or resume_action with
nothing to act on with no_focus. See Write tools and
Focus.
Conventions
- Slugs, not paths. Tools take slugs (
surrogate-model), matching the CLI. - Substring matching for completing actions/milestones and resolving waiting-on items, exactly as on the CLI.
- The same rules apply. The five-project cap, append-only notes, auto-logged project-state history, and commitments aggregation all hold — the MCP server is not a back door around them.
When a call is rejected
Cuaderno splits failures along the line MCP itself draws, and the distinction is worth knowing because the two arrive in different places in the response.
A rejection you can act on comes back as a tool result with isError: true — not as a JSON-RPC
error. The response carries no error member at all, and the payload is the single text content
item, JSON-encoded exactly as a successful result’s payload is:
{ "isError": true, "content": [{ "type": "text", "text": "…" }] }
Decoding that text gives the rejection itself. This is real output from complete_action with a
query matching two bullets:
{
"code": "ambiguous_action",
"details": {
"candidates": [
"Draft the methods section (deep)",
"Revise the methods appendix (light)"
],
"query": "methods",
"slug": "ablation-study"
},
"message": "ambiguous action match for 'methods' on project 'ablation-study': [\"Draft the methods section (deep)\", \"Revise the methods appendix (light)\"]"
}
Three fields, and each has a job:
| Field | For |
|---|---|
code | A stable snake_case discriminant to branch on — ambiguous_action, state_too_long, project_cap_reached, field_not_settable, … |
message | The human-readable sentence, for showing a person |
details | The structured fields needed to recover — candidates to pick from, chars/max to condense against, active_projects to park one of |
details is the part that matters for automation. An ambiguous query hands back its candidates as
an array, so a client picks from a list rather than parsing them out of a sentence.
Five project codes carry details a client is likely to branch on:
| Code | details | When |
|---|---|---|
project_not_active | slug, status (parked, completed or dropped), and closed when the map carries a date | A parked status means a focus verb (start_action, resume_action, switch_action, start_unplanned_action, switch_unplanned_action) or park_project was refused: starting or picking up work needs an active map, so activate it first only if the user wants to work on it now. Every other project-body verb (add_action, update_project_state, add_milestone, …) works on a parked map. A closed status means a body verb on a completed or dropped project, or a close repeating the outcome it already has: the project is finished, unless the user wants it back. |
project_cap_reached | current, max, active_projects | A project would begin to count with the counted projects at the cap (activate_project, or taking back an active project with hand_off_project). current and active_projects are the projects that count, active and held by no one else, so each listed slug is one whose parking, completion or dropping frees a slot: offer those, or leave the project as it is. |
ambiguous_project | slug, candidates: [{path, note_type}] | The slug names maps in more than one of projects/, projects/_parked/ and projects/_done/<year>/. Nothing can pick for you: keep one of the files by hand. |
project_in_focus | slug, action | A project was to be handed off while action, one of its actions, is in focus: a project is not handed off while you are in the middle of its work. Pause the action first, if the person wants to hand it off now. |
empty_field (for held_by) | field: "held_by", value, remedy: "take_back" | The holder given names the person or nobody (me, tbd, someone, …, the vault’s [projects] holder_sentinels). If they cannot name who has the project, it is theirs: take it back rather than hand it off. Any other empty_field carries field alone. |
The test is simply whether a different argument would change the outcome. A slug that matches no
note is a rejection (not_found), and its message carries the list of slugs that do exist. A note
missing the section a tool writes into is a rejection. So is a name that is already taken.
A mechanical failure stays a JSON-RPC error (-32603): a disk that would not write, permission
denied, a contended write lock, an index that would not answer, a transaction that rolled back.
Nothing the caller passed can fix those, so there is nothing to branch on. Malformed calls — an
unknown tool, unparseable arguments — are protocol errors too (-32602), per the spec.
The practical upshot for a client: read isError on the result, not just the presence of error.
A rejection is a normal, expected answer to a call that was itself well-formed.
Slug rejections
An explicit slug on a create tool (Creation and lifecycle) is
refused with one of four codes, each leaving the vault untouched. They are the same objects the CLI
prints under --json.
| Code | details | When |
|---|---|---|
invalid_slug | slug, why | The slug breaks the shape rule (lowercase a-z, 0-9, single -, at most 50 characters, not YAML-typed); why names the part. Fix the shape, or ask the person for another name. |
slug_taken | slug, holder, kind (stem|folder|alias) | Another note already holds the name. If holder is the note you meant, use it; otherwise ask the person for another name. alias means the name is a former name another note still lists. Do not suffix it yourself. |
portfolio_slug_mismatch | question_slug, slug | create_portfolio with question set to a question’s slug and a slug that is not that slug. Drop slug or pass the question’s. |
slug_given_twice | field | create_custom_note with slug and a slug entry in fields. Pass one of them. |
Building skills on top
Multi-step rituals (a morning orientation, a guided weekly review) are best wrapped as Claude skills that call these tools in sequence. See Using with Claude skills.
The HTTP server
cdno-mcp-server serves the same tool catalogue as the stdio cdno-mcp, over the MCP
Streamable HTTP transport, for clients that reach your vault remotely — most notably Claude’s
custom-connector infrastructure, which connects from Anthropic’s cloud for every surface
(web, desktop, mobile).
cdno-mcp-server --vault ~/vault # listens on 127.0.0.1:8787, endpoint /mcp
Security model — read this first
The binary issues no OAuth of its own, on purpose. Static bearer tokens are not spec-legal
for remote MCP connectors; real deployments terminate OAuth 2.1 at an identity-aware proxy
(for example Cloudflare Access with Managed OAuth in front of a Cloudflare Tunnel). The server’s
own contribution is origin-side validation of the identity assertion the proxy injects
(Cf-Access-Jwt-Assertion): RS256 against the team’s JWKS, strict issuer/audience/expiry, fail
closed — configure it with CDNO_ACCESS_TEAM_URL and CDNO_ACCESS_AUD. If the JWKS cannot be
fetched at startup, the server refuses to start rather than serve unauthenticated.
Without that configuration, cdno-mcp-server refuses to bind anything but loopback.
Be precise about what that guarantees: the process only accepts connections arriving on its own
loopback interface. It cannot detect a tunnel or SSH forward that bridges the port outward —
never bridge this port without the authenticating proxy in front. The server logs a warning
at startup to the same effect whenever it serves real vault data unauthenticated. Configuring
the JWT validation is exactly what lifts the non-loopback restriction (e.g. binding 0.0.0.0
inside a container).
Flags
| Flag | Env | Default | Purpose |
|---|---|---|---|
--vault <path> | CUADERNO_VAULT_PATH | cwd | Vault root |
--bind <addr> | CDNO_MCP_BIND | 127.0.0.1:8787 | Listen address (non-loopback refused until #302) |
--allowed-host <host> | CDNO_MCP_ALLOWED_HOSTS (comma-separated) | — | Extra Host header values to accept on top of the loopback defaults (DNS-rebinding protection). A public deployment adds its hostname |
--smoke | — | off | Serve a single echo tool holding no vault handle — prove tunnel/auth infrastructure end-to-end with zero vault exposure |
--read-only | — | off | Advertise only the context-gathering read tools; mutating tools are absent from the dispatch table entirely |
--reconcile-interval-secs <n> | CDNO_MCP_RECONCILE_INTERVAL_SECS | 300 | Periodic index reconciliation; 0 disables |
--git-checkpoint-interval-secs <n> | CDNO_MCP_GIT_CHECKPOINT_INTERVAL_SECS | 60 | How often the git sweep runs. 0 disables it; warns and no-ops when the vault isn’t a git repo |
--git-checkpoint-mode <mode> | CDNO_MCP_GIT_CHECKPOINT_MODE | commit | What the sweep does with a dirty tree: commit here, or nudge-only — see The recovery trail |
--sync-nudge | CDNO_MCP_SYNC_NUDGE | off | Touch a sentinel file after every verified write so an external sync agent reacts at once instead of on its own timer — see Pairing with a sync agent |
--sync-nudge-path <path> | CDNO_MCP_SYNC_NUDGE_PATH | <vault>/.git/cdno-sync.nudge | Where that sentinel lives. Setting it does not by itself enable nudging |
--access-team-url <url> | CDNO_ACCESS_TEAM_URL | — | Cloudflare Access team URL (JWT issuer + JWKS host). Requires --access-aud; activates origin JWT validation and lifts the loopback-only restriction |
--access-aud <tag> | CDNO_ACCESS_AUD | — | The Access application’s AUD tag (expected aud claim). Requires --access-team-url |
The recovery trail
Exposing write tools remotely means anything a confused or prompt-injected session does lands in
your vault. The damage limit is that every mutation ends up in a git commit you can diff and
revert. The sweep is what provides it: on an interval it takes the vault write lock, and if the tree
is dirty it commits everything as cdno-mcp checkpoint (N path(s)). It is a sweep rather than a
per-write hook, so out-of-band edits — the CLI, your editor, a sync tool — join the trail too.
What must hold is that something commits. Which actor does is configurable:
| Mode | Who commits | How |
|---|---|---|
| interval (default) | this server | --git-checkpoint-interval-secs <n>, default 60 |
| nudge-only | an external sync agent | --git-checkpoint-mode nudge-only |
| disabled | nobody | --git-checkpoint-interval-secs 0 |
Reach for nudge-only when an agent already owns the repository’s history. Per-minute checkpoint commits would fight it: two git actors in one working tree, and the agent’s coalesced, unit-of-thought commits buried under machine noise. In this mode the sweep still runs and still takes the lock, but it commits nothing — on a dirty tree it touches the sync-nudge sentinel and leaves the change exactly where it found it, unstaged, for the agent to pick up.
cdno-mcp-server --vault /srv/vault --git-checkpoint-mode nudge-only
The sentinel path is the one --sync-nudge-path sets, so the sweep and the per-write nudge always
agree on it. You do not need --sync-nudge as well: that flag governs the per-write signal, and
the two are useful together (writes nudge immediately; the sweep catches anything that arrived out
of band) but independent.
Both non-default modes hand the trail to somebody else, and the server says so at startup —
nudge-only logs a warning naming the sentinel and stating that this process commits nothing, and
0 warns that nothing in the process commits at all. If no agent is running, either is equivalent
to having no recovery trail.
Only cdno-mcp-server sweeps. The stdio binary has no checkpoint loop and none of these flags.
When the sweep stops
The sweep’s job is to be the thing you can fall back on, so it must never fail quietly. Two ways it can stop, both loud:
-
A tick that runs and finds trouble — a non-zero
git, the vault write lock busy, someone else’s paused merge or rebase — logs and retries on the next tick. Only agitthat cannot be executed at all, five times running, disables the loop, with an error saying so. -
A tick that never runs at all. The sweep does its work on a pool of worker threads, and if the process cannot get one, the tick is queued and the loop waits. Nothing is committed and, until this was fixed, nothing was logged either — the endpoint stayed up and tool calls kept answering, so nothing outside could tell. A tick that overruns three sweep intervals (never less than 30 seconds) now logs:
ERROR git checkpoint sweep STALLED: a tick has not completed, no further tick can start, and so NOTHING in this process is committing — writes are no longer being recorded. ...It repeats while the tick stays stuck, and logs a recovery line if it completes. Alert on it: the two realistic causes are a process out of threads (see Thread budget) and a wedged
gitinvocation, and the process cannot tell them apart from inside — but either way the vault has stopped being recorded.
Thread budget and pids_limit
A container sized with pids_limit counts every OS thread this process holds, not just processes.
The server bounds itself so that number is knowable, and logs it at startup:
INFO runtime thread budget workers=8 max_blocking_threads=16 max_os_threads=25
max_os_threads is the ceiling: one main thread, one async worker per CPU the process can see, and
a fixed pool of 16 for the blocking work (vault reads and writes, the reconciliation pass, the
checkpoint sweep). Size pids_limit above that number with room for whatever else shares the
container — a healthcheck that shells out needs to fork too, and a container that cannot fork
reports unhealthy and refuses docker exec while the server itself carries on serving.
Note that workers follows the CPUs the process can see, which on Linux is CPU affinity, not a
cgroup CPU quota: a small container on a large host still gets a worker per host core. Read the
number off the log line rather than assuming it.
Pairing with a sync agent
A common shape is two clones of the vault repository — an always-on host running this server, and a laptop — with an external agent on the host owning the commit-and-push loop. That agent normally polls: it wakes on its own timer, sees a dirty tree, and commits. A write that landed a second after the last poll waits out the whole interval.
--sync-nudge closes that gap. After every write the server has verified,
it rewrites a sentinel file, changing both its modification time and its contents. The agent watches
that one path — launchd WatchPaths, inotifywait, fswatch, whatever it already uses — and acts
immediately.
cdno-mcp-server --vault /srv/vault --sync-nudge
# → touches /srv/vault/.git/cdno-sync.nudge after each verified write
The contract is deliberately narrow:
- Off by default. A deployment with no agent has nobody to signal.
- One-way. The server writes the sentinel and never reads it, so an agent that is absent, stopped, or slow costs nothing but latency.
- Only after a verified write. A write that failed, or that could not be read back, leaves the sentinel alone — an agent woken by writes that did not happen learns to ignore the signal.
- Never fatal. A sentinel that cannot be written is logged and skipped; the write still succeeds.
- Never content. The file holds a unix timestamp and nothing else. It names no note.
It lives under .git/ on purpose: git will not track it, and no tool that mirrors the working tree
will carry it, so the signal cannot leak into the vault or across machines. --sync-nudge-path
moves it if your agent needs it elsewhere; parent directories are never created.
Only cdno-mcp-server has this. The stdio binary is a local session with no agent on the other side
of it, and offers no such flag.
Index freshness
Unlike a stdio session, this process is long-running while other writers — the CLI, editors, sync
tools — mutate the Markdown underneath it. Markdown is the source of truth and the index is a
cache, so the server re-runs the reconciliation pass on the configured interval as the correctness
backstop. Out-of-band edits become visible to search_notes and the context tools within one
interval at most.
Timezone — set TZ on the host
The server timestamps everything it writes — log lines, daily entries, tracking entries — with the
process’s local time (chrono::Local::now()). A container or host with no zoneinfo database and
no TZ set makes chrono fall back silently to UTC, so remote writes land hours behind the wall
clock with no error. Any deployment must therefore ship a zoneinfo DB (tzdata on Alpine, already
present on most distros) and set TZ, e.g. TZ=Europe/Stockholm.
At startup the server logs the offset it resolved, next to the “vault opened” line:
INFO local time zone resolved (server timestamps use process-local time) local_offset=+02:00 sample_local_now=2026-07-06T14:30:00+02:00
Check this line after deploying: a local_offset=+00:00 you didn’t intend is the tell-tale of a
missing tzdata/TZ. (A host legitimately in UTC is fine — the line is factual, not a warning.)
Transport details
- Endpoint:
POST /mcp. Clients must sendAccept: application/json, text/event-stream(the Streamable HTTP spec requires both). - Stateless JSON mode: every request is self-contained; responses are plain
application/json(no SSE streams, no session ids).GET/DELETEon/mcpreturn405. - Guardrails: request bodies are capped at 1 MiB and in-flight requests are bounded; the
Hostheader is validated against the allowlist (403 otherwise).
Context-gathering tools
Read-only tools an assistant uses to understand your vault before acting. None of these mutate anything. Inputs marked optional may be omitted.
| Tool | Inputs | Returns |
|---|---|---|
get_orientation | energy? (deep|medium|light) | Commitments due soon (milestones and action dates of active and parked projects, standalone commitments, stewardship lines), active projects, lapsed stewardship habits, and a suggested starting point. Also focus (the current_focus shape, or null) and, on each project, last_paused (its most recent unresolved pause, or null) beside top_action, plus held_by, counts, waiting_on_empty and core_question as in the review fields, and slots (as on list_projects). Parked projects are not listed; suggest the day’s work from the projects that count. The MCP form of cdno orient, which renders neither focus nor last_paused. |
get_project_context | project (slug) | A project’s state, next actions, milestones, waiting-on items, and links. Resolves active, parked and closed (_done/<year>/) projects. Also last_paused, as on get_orientation, so the re-entry hint is there when the project is opened and not only in the morning. The frontmatter carries held_by, and the payload counts. |
get_portfolio_contents | portfolio (slug) | Portfolio metadata plus its evidence inventory. |
get_weekly_context | date? (any day in the week) | The weekly note’s sections (Wins, Challenges, One Improvement, This Week’s Goal), plus the week’s logs, completed_actions, the projects completed or dropped that week (closed_projects) and project state changes. Also projects: every active project, handed-off ones included, with the review fields. |
get_monthly_context | date? | Monthly context for a strategic scan, including the past 30 days’ completed_actions as wins patterns, the projects completed or dropped in those 30 days (closed_projects), and every parked project (parked_projects). Parked entries and the stuck projects (active, unchanged for two weeks, handed-off ones included) carry held_by, counts and waiting_on_empty, as in the review fields. Also slots. |
get_stewardship_tracking | stewardship, activity, period? (e.g. 30d, 6m) | Tracking entries for a stewardship/activity over a window, plus the activity’s declared contract in spec (record key, group field, and each metric’s type, unit, aggregate, and derived expression when it is computed rather than recorded — write that metric’s operands, never a field of its own name) when the vault declares one under [tracking.<activity>]. spec is null for an undeclared activity. Also returns series: this activity’s numeric trends, one per (group, metric) it declares, each point already reduced by that metric’s own aggregate, scoped and windowed by the same activity and period as the entries. |
get_active_questions | domain? (research|life) | Active question notes, optionally filtered by domain. |
get_commitments | lookahead_weeks? (default 2) | The aggregated commitments view; overdue always included. A project’s milestones and action due dates appear while it is open, active or parked, and leave once it is completed or dropped; a standalone commitment appears whatever its project’s status. |
current_focus | — | The action most recently started or resumed, unless it has since been completed, dropped or paused (focus is one slot), or null. Returns project, action, started, date, carried and origin (below). Replayed from the ## Logs of today and the [focus] carry_over_days days before it (default 1), so a start made from the CLI counts too, as does one written by hand in the log’s full shape (- **HH:MM**: started [[slug]] — text — stamp and em dash U+2014 both required; see cdno now); a completion, a drop or a pause clears it, and a promote_action in between renames it to the new note, keeping the start time. The MCP form of cdno now. |
list_projects | include_closed? (default false) | Active and parked projects with their frontmatter (held_by among it) and counts, plus the slot budget slots; with include_closed: true, completed and dropped projects too, under closed. |
list_note_types | — | Every note type — the built-ins plus any config-defined [note_types.*] custom type — with its folder, required/optional fields, typed [schemas.*] field specs, template, and supplied placeholders. Call before create_custom_note to discover a vault’s custom types. |
read_daily_note | date? (default today) | The daily log for a date. |
read_weekly_note | date? (default this week) | The weekly note for an ISO week. |
read_monthly_note | date? (default this month) | The monthly note for a calendar month. |
read_note | note (path, slug, type:slug or journal date) | Any note, whole: path, note_type, frontmatter, body (uncapped), content_hash, backlinks and headings. Takes the references cdno open takes, plus a path without its .md. |
search_notes | query, note_type?, from?, to?, portfolio?, limit? (default 20) | Ranked full-text hits. The MCP form of cdno search. |
lint | — | Vault-wide problems: frontmatter, broken wikilinks, a link that resolves only through an alias (a former name; the message names the current one), an alias that another note’s file stem shadows or a folder note’s name contests, attachment pairing, a project whose status disagrees with its folder, a custom template that lacks a key its built-in has gained, and lines the canonical parsers silently skip — malformed stewardship-dashboard bullets and daily-log focus markers (started, paused, resumed, action promoted on) cdno now will not read back, plus the same check on a renamed or relinked line and a warning for a link in its was: value. |
triage_inbox | — | Pending inbox captures awaiting triage. |
Review fields
Each element of get_weekly_context.projects:
{"slug": "harbour-survey", "held_by": "the harbour office", "counts": false,
"waiting_on_empty": false, "core_question": null, "changed_this_week": true,
"focus_days_while_held": 2}
| Field | Meaning |
|---|---|
held_by | The name of whoever else is moving the map, trimmed, only when held_by: names someone else. null when the map is the person’s own (no held_by:, or a value on [projects] holder_sentinels) or the value is unreadable (not a plain string). |
counts | true when the map holds one of the max_active_projects slots: active, and naming no one else as its holder. Branch on this, never on the name. |
waiting_on_empty | true when ## Waiting On is missing, empty, or only (nothing yet). A holder over an empty Waiting On is a parked map wearing a name. |
core_question | The map’s core_question: as written, or null. |
changed_this_week | true when the map file was modified after the end of the Sunday before this ISO week’s Monday. A tool that touches the file without changing it also sets it, so it prompts a question, never a conclusion. |
focus_days_while_held | For a handed-off map (counts false), the number of distinct days from Monday to today with a focus started or resumed on it; 0 for any other map. Days, not starts. It is evidence for the person to weigh: no log line records who did the work. |
The weekly review asks the person’s own projects whether they moved, and a handed-off project that
changed this week what came back and what awaits the person’s decision. get_monthly_context’s parked and stuck
entries carry held_by, counts and waiting_on_empty with the same meanings, so the monthly
review tells a project shelved for someday from one someone else has.
Slots and counts
slots, on list_projects, get_orientation and get_monthly_context:
{"active": 3, "counted": 2, "cap": 5}
| Field | Meaning |
|---|---|
active | The open projects with status: active, handed off or not: every active project shown. |
counted | The active projects that hold a slot, naming no one else in held_by:. The cap is checked against this number. |
cap | The configured max_active_projects. |
Each list_projects row (active, parked and closed) and the get_project_context payload carry
counts, with the meaning in the review fields, and their frontmatter carries
held_by exactly as written, since it is the frontmatter view: a sentinel comes through as its
text there, while the summaries’ held_by reads null for it. Branch on counts, never on the holder’s name. list_projects stays a
listing that reads no map body, so its rows carry no waiting_on_empty; the orientation projects
carry it.
Focus fields
current_focus, the focus field of get_orientation and the focus field every
focus-carrying write result share one shape:
{"project": "surrogate-model", "action": "Draft the methods section (deep)",
"started": "08:50", "date": "2026-10-04", "carried": false,
"origin": {"started_at": "2026-10-03T14:05"}}
| Field | Meaning |
|---|---|
project | Project slug. |
action | The bullet text exactly as logged, energy suffix and all: the string complete_action expects back. After a promote_action it is the new note’s link, [[actions/<slug>]] (energy). |
started | HH:MM of the open marker (the started, or the resumed that re-anchored it). |
date | The day of that marker, YYYY-MM-DD. |
carried | true when date is not today: the focus was left open on an earlier day. |
origin | {started_at} of the earlier start a resume continued, else null. A resume after a pause has none. |
null means nothing is open, wherever it appears.
last_paused, on get_orientation’s projects and on get_project_context:
{"project": "surrogate-model", "action": "Draft the methods section (deep)",
"title": "Draft the methods section", "at": "2026-10-04T11:13", "date": "2026-10-04",
"next": "pick up at \"Prior approaches\"", "reason": "lunch"}
It is the project’s most recent pause that no later started, resumed, completion, drop or
promotion of the same action followed, looked for in the last
paused_lookback_days days; next and reason are null when the pause
had none. Quote next back to the person when they pick the project up. The server instructions
tell an agent how to use the focus; see Write tools.
Notes
- Dates are
YYYY-MM-DD. Week-scoped tools accept any day within the target ISO week; month-scoped tools accept any day within the target calendar month. search_notesreturns the same hit shape as the CLI:path,note_type,title,snippet,score. See JSON output.completed_actions(weekly and monthly) covers both forms an action takes. One with its own note carriesslugandpathalongsidesource: "note"; an inline bullet — the default form, and so the common case — carriessource: "bullet"with both null. A client must expect null there. A completion that has both a note and a log line is listed once, and dropped actions never appear. The list is read from the daily notes directly rather than from the cappedlogsfield, so a completion early in a busy week is not lost to that cap.read_notereads any note, of any type.content_hashis the hash of the bytes just read, the value to hand back asexpected_hashwhen revising the note, so a change made in between is detected rather than overwritten.backlinkscomes from the index, which every cuaderno write updates; a link made by editing a file elsewhere appears once the index next reconciles. A slug several notes share is refused with codeambiguous_slugand the candidates’ paths indetails.candidates; a reference matching nothing is refused with codenot_found.headingslists every heading in the body with inline markup stripped, the formrevise_note’ssectiontakes. To follow a heading link such as a concept’sorigin([[journal/2026/daily/2026-09-02#Woodbury identity]]), pass the part before the#and find the entry under that heading: the whole link, anchor included, is refused asnot_found. Whennotewas a former name of a renamed note, the payload addsfound_by_former_name(the name typed) andpathshows where the note is now: use the current slug from then on.- These pair naturally with the write tools: read context, propose an action, then write it.
Write tools
Tools that mutate the vault. Each returns a result describing what was written. The same business rules as the CLI apply (append-only notes, auto-logged project state, the project cap).
Every write is verified
A tool result that only said “success” could not be told apart from a write that silently never landed — which over a remote connection is exactly how a lost note goes unnoticed. So every write tool re-reads its target before answering, and the result carries the evidence:
{
"path": "journal/2026/daily/2026-08-26.md",
"message": "Logged to journal/2026/daily/2026-08-26.md",
"verification": {
"verified": "content",
"bytes_written": 412,
"content_hash": "84bc0919e867576f",
"appended_tail": "- **09:14**: baseline sweep finished\n"
}
}
| Field | Meaning |
|---|---|
verified | content — the file was re-read; or removed, for discard_inbox_item, where the check is that the file is gone |
bytes_written | Size of the file on disk after the write. 0 for a removal |
content_hash | The note’s content hash (below) — null for a removal |
appended_tail | For append-shaped writes (append_to_log, start_action, note_to_daily), the tail of the section the text went into — see below. null elsewhere |
If the target cannot be read back, the tool returns an error rather than a success. The wording says the write is unverified, not failed: it may still have landed, so the right response is to re-read the note, not to blindly repeat the write.
The appended tail
appended_tail is scoped to the section the write targeted, not to the last bytes of the file.
append_to_log and start_action both write into ## Logs, so that is the section you get back.
note_to_daily writes into both ## Notes and ## Logs; its tail is the last 512 bytes of
## Notes, which end with the entry just written (the pointer line is already in the result’s
log_line).
(start_unplanned_action also logs, but it rewrites the project map in the same commit and so
verifies as a whole-file rewrite — its appended_tail is null.)
The distinction matters because ## Logs is not always last. Cuaderno pins the effective daily
template’s last ## section to the bottom of the note — for the built-in template that is ## Logs,
but a custom .cuaderno/templates/daily.md ending in, say, ## Reflection keeps Reflection last and
leaves the log line in the middle of the file. Reading the end of the file would then show you
Reflection’s text while claiming to be the line that landed.
If the section cannot be located — an unparseable note, a ## Logs heading that is missing or
duplicated — the field is null. The write is still verified by bytes_written and content_hash;
only the extra evidence is withheld, because a window over the wrong bytes is worse than no window.
The content hash
content_hash is the same non-cryptographic xxh3-64 fingerprint (16 lowercase hex characters) the
index uses for change detection, so a client can recompute it over a note it has read and compare.
Two uses in practice:
- confirm a note is byte-identical to the one the server saw;
- notice a no-op.
update_project_statewith a state that already matches deliberately writes nothing and still reports success — an unchangedcontent_hashacross two calls is how you tell.
It is a change detector, not tamper evidence: it does not defend against someone who also chooses the content.
Logging, capture, triage
| Tool | Inputs | Effect |
|---|---|---|
append_to_log | text | Append a line to today’s daily note. (cdno log) |
capture | text | Drop a raw note into inbox/. (cdno capture) |
discard_inbox_item | slug | Clear a triaged capture (slug from triage_inbox). |
When a focus is open, append_to_log, capture, note_to_daily and discard_inbox_item tag what
they write with it; see The during: tag.
Evidence
| Tool | Inputs | Effect |
|---|---|---|
file_to_portfolio | portfolio, source, origin, content?, attach?, vars? | File evidence into a portfolio; attach is a server-side path to a non-Markdown artefact (vars is ignored on the attach path). (cdno file) |
Projects, actions, milestones, waiting-on
Every tool in this table takes an open project, active or parked: a parked map is a complete map,
so add_action, update_project_state, the milestone and waiting-on tools, set_core_question,
promote_action, complete_action, drop_action and hand_off_project write to it as it is.
Never activate a parked project just to write on it; the cap is checked only when a project begins
to count (activate_project, or a take-back of an active project). Only starting or picking up
work (start_action, start_unplanned_action, switch_action, switch_unplanned_action,
resume_action) needs an active map, as does park_project; on a
parked one they return project_not_active with status: "parked" (see the
rejection codes). A completed or dropped project takes none of these writes.
| Tool | Inputs | Effect |
|---|---|---|
update_project_state | project, new_state | Rewrite the Current State (auto-logs the previous). |
set_core_question | project, core_question?, clear? | Set the project’s core question (bare questions/<domain>/<slug> target, not [[…]]); clear: true detaches. Auto-logs the previous value. |
hand_off_project | project, holder?, take_back? | Record whose move an open (active or parked) project is: holder writes held_by: "<name>" and the project stops counting against the cap; take_back: true writes held_by: null. Exactly one of the two. Logs holder on [[slug]] with was:/now: lines. Refused with empty_field for a name on the vault’s [projects] holder_sentinels, with project_in_focus while an action on the project is in focus, and, for a take-back that would make an active project count at the cap, with project_cap_reached. Taking back a parked project only clears the name. |
add_action | project, title, energy, with_note?, slug?, vars? | Append a next action; with_note also scaffolds a manifest note (slug and vars apply only then; without with_note, slug is ignored). |
promote_action | project, query, slug?, vars? | Promote a bullet to a manifest note (substring match); slug names the new note’s file (the slug rule). |
start_action | project, query | Log that work on an existing bullet is starting. Logs the resolved bullet text, so the later close pairs with it. Errors when query matches nothing — it will not create the action. Refused with focus_open while another action is in focus (below). |
start_unplanned_action | project, title, energy | Add the bullet and start it, in one commit, for work that was on no map. Separate from start_action on purpose: a fallback would turn a typo into a new action. |
pause_action, switch_action, switch_unplanned_action, resume_action | see Focus tools | Move the one focus slot: set it aside, move to another action, or pick work up again. |
complete_action | project, query | Complete an action; archives its note if any. |
drop_action | project, query, reason? | Close an action without recording it as done (superseded, abandoned, reprioritised); archives its note as status: dropped. |
add_milestone | project, title, target_date?, hard? | Add a milestone; hard counts it in commitments and requires target_date. Omit target_date for a condition-gated milestone (target: TBD), which stays out of commitments. |
complete_milestone | project, query | Complete a milestone (substring match). |
drop_milestone | project, query, reason? | Remove a milestone, with the lines indented beneath it, without recording it as met (superseded, mis-typed, not happening); logs milestone dropped on. Completed bullets are never matched. |
add_waiting_on | project, description | Add a waiting-on blocker. |
resolve_waiting_on | project, query | Resolve a waiting-on item (substring match). |
Focus tools
Four tools move the focus, the one “what I am on” slot that current_focus
reads back. They are the MCP forms of cdno action pause, switch and resume.
Each writes one entry in today’s ## Logs and, like every write, is verified.
| Tool | Inputs | Effect |
|---|---|---|
pause_action | next?, reason? | Sets the current focus aside without finishing it. Takes no project or query: it pauses whatever current_focus shows, with nothing matched against the map. Logs action paused on [[slug]] — <text> with optional indented next: and reason: lines. Result adds paused (the focus that was paused). |
switch_action | project, query, next?, reason? | Pauses the current focus and starts an action already on the map, in one commit. project and query name the action to switch to; next and reason describe the one set aside. With nothing in focus it is a plain start and paused is null. |
switch_unplanned_action | project, title, energy, next?, reason? | The same, for work on no map yet: adds the bullet and starts it. Separate from switch_action for the reason start_unplanned_action is separate from start_action: a query that matches nothing must never become a new action. |
resume_action | project? | Re-anchors a focus carried over from an earlier day, or reopens the latest pause within paused_lookback_days; with project, that project’s carried focus, else its latest pause. Logs resumed [[slug]] — <text>. |
switch_action and switch_unplanned_action return paused (the focus that was set aside, or
null) and started (the new focus). resume_action returns resumed (the focus now open) and
resumed_from:
{"resumed": {"project": "surrogate-model", "action": "Draft the methods section (deep)",
"started": "11:12", "date": "2026-10-04", "carried": false, "origin": null},
"resumed_from": {"kind": "paused", "date": "2026-10-04",
"next": "pick up at Prior approaches", "reason": "call"}}
resumed_from.kind is carried or paused, next and reason are the pause’s (always null for a
carried focus). With project, a carried focus wins over that project’s pause, so read kind and
resumed.action back to the person rather than assuming the pause they were shown, and read
next out as the re-entry cue. pause_action and resume_action are for the person’s word:
the tool descriptions tell an agent never to call them unasked.
Rejections: focus_open and no_focus
Two refusals are returned as coded rejections, so an agent can act on them without parsing prose. Nothing is written.
focus_open comes from start_action, start_unplanned_action, switch_action,
switch_unplanned_action and resume_action:
{"code": "focus_open",
"message": "An action is already in focus. Ask the person before switching; do not retry.",
"details": {"focus": {"project": "surrogate-model", "action": "Draft the methods section (deep)",
"started": "09:10", "date": "2026-10-04", "carried": false},
"attempted": {"project": "other-project", "query": "CI"},
"same_action": false,
"remedy": "switch_action"}}
details key | Meaning |
|---|---|
focus | What is open: project, action, started, date, carried. |
attempted | What the call asked for: {project, query}, or {project, title} for an unplanned form. Absent from resume_action’s rejection, which attempts no target. |
same_action | true when the call named the action that is already in focus. |
remedy | The tool to call, on the person’s word: switch_action (something else is in focus), resume_action (the same action, carried over from an earlier day), or already_focused (the same action, started today: nothing to do). |
The message tells the agent to ask the person before switching and not to retry. The server never switches on its own.
no_focus comes from pause_action (Nothing is in focus to pause.) and resume_action (Nothing to resume: no focus carried over and no pause.), with empty details. Tell the person there is
nothing to act on rather than retrying.
focus on write results
The success payload of these eighteen tools carries a focus field: start_action,
start_unplanned_action, switch_action, switch_unplanned_action, pause_action,
resume_action, complete_action, drop_action, promote_action, add_action, append_to_log,
capture, note_to_daily, park_project, activate_project, complete_project,
drop_project and hand_off_project. It is the focus as current_focus reads it right after the
write, so an agent sees what is open without a second call: the same shape, and null means
nothing is open. It is read in the same call that verifies the write, and a focus that cannot be
read after a write has landed comes back as null rather than as an error, because an error would
tell the agent that a successful write failed. Every other write tool’s result is unchanged.
The server never refuses a write for being outside the focus. How an agent should behave when a request is, is stated once in the server’s instructions rather than in each tool description: compare at project level, say one sentence only on a mismatch, offer to capture it, and treat the person’s explicit “let’s switch to Y” as consent. See Focus.
The during: tag
When a focus is open at write time, a carried one included, the writes that park a thought say which project it was written during:
-
append_to_logandnote_to_dailyappend an indented continuation to the log line:- **11:12**: checked the mesh during: [[surrogate-model]](
note_to_dailyalso echoes the tag on a second line of thelog_lineit returns.) -
captureaddscaptured_during: surrogate-modelto the new inbox item’s frontmatter. -
discard_inbox_itemcopies that tag from the item onto the log line it writes, so it outlives the file.
With no focus open nothing is written differently, and a focus that cannot be read writes untagged rather than failing. The tag is written by the server; an agent does not add it to its own text.
Commitments and tracking
| Tool | Inputs | Effect |
|---|---|---|
create_commitment | title, due, context, project?, stewardship?, slug?, vars? | Create a standalone commitment note; slug names its file (the slug rule). |
complete_commitment | commitment (slug) | Mark a commitment done and archive it. |
drop_commitment | commitment, reason? | End a commitment that was not kept; stamps status: dropped, clears completed, archives it. Never appears as completed work. |
reschedule_commitment | commitment, due | Move an active commitment’s due date, logging both the old and new dates. Refuses an unchanged date. |
complete_periodic | stewardship, title, at? | Complete one occurrence of a stewardship’s periodic commitment, rolling next: forward by that line’s recurrence. Anchored to the due date, so completing early never drags the schedule earlier. |
create_tracking_entry | stewardship, activity, routine?, content?, vars?, metrics?, date? | File a tracking note under an expanded stewardship. metrics is a JSON object merged into the entry’s frontmatter — a scalar per reading ({"balance": 1240.5}), or an array of flat records when one entry holds several comparable items ({"detail": [{"subject": "harmony", "minutes": 25}]}); a scalar whose key is declared under [schemas.tracking.fields] is type-checked, and a key naming the note’s identity (type, stewardship, activity, date) is refused. date files the entry for a past day (bounded to 50 years back, 1 year ahead). A second call for the same (activity, date) merges into the first: content appended, metrics folded in. Records carrying a stable id replace the record with that id; records without one append, so re-sending a payload without ids double-counts summed metrics. Either way the write is journalled to today’s daily log. |
Revising a note
| Tool | Inputs | Effect |
|---|---|---|
revise_note | note, reason, and either body + expected_hash or section + content | Refine a mutable custom note (a concept, say) in place, logging the revision to today’s daily note in the same write. (cdno note revise) |
revise_note takes exactly one of two forms:
| Input | Meaning |
|---|---|
note | Which note: any reference read_note takes — a vault path with or without .md, a bare slug, or type:slug. |
body | Whole-body form. The note’s new body, everything after the frontmatter (which is kept as it is), written verbatim. |
expected_hash | Required with body: the content_hash read_note returned. A blank value counts as missing. Ignored with section. |
section | Section form. Heading text of the section to upsert, as read_note’s headings lists it, without the # markers. An existing section is replaced together with its sub-sections; a missing one is appended as ## <section>. No hash is needed. |
content | The section’s new text, without its heading. Only with section. |
reason | Required. Why the note was revised, in a short clause; the assistant drafts it from what it changed. It becomes the daily-log line revised [[<path>#<section>]] — <reason>, or revised [[<path>]] — <reason> for a whole body. |
Text identical to the note’s current text writes and logs nothing and returns changed: false.
The result extends the usual write result with changed, new_hash (the note’s hash after the
call: pass it as expected_hash on a follow-up revision rather than reading again), log_line
(the line written, without its timestamp, or null) and section_target (the anchored link the
log line points at, for a section revision); verification is null when nothing changed.
Refusals (a rejection with these codes; nothing is written):
| Code | When |
|---|---|
note_not_revisable | The note is of a built-in type (its sections belong to its own tools) or of a custom type declared append_only = true. |
stale_revision | A whole-body revision whose expected_hash no longer matches the bytes on disk: the note changed since it was read. details.actual is the current hash; read the note again and redo the edit on the fresh text. |
revision_invalid | The section heading is empty, spans lines, contains [, ], | or #, or starts with ^; or content holds a heading at the section’s own level or above, which would restructure the note. |
ambiguous_section | The section heading matches more than one heading in the note. |
not_found / ambiguous_slug | The note reference matches no note, or several (with their paths in details.candidates), as for read_note. |
A blank reason, body together with section, section without content, content without
section, or body without expected_hash is refused as an invalid argument (-32602) naming
the field.
Renaming a note
| Tool | Inputs | Effect |
|---|---|---|
rename_note | note, to or relink: true, reason?, dry_run?, expected_plan?, skip_sync_check? | Rename a note’s slug and follow every reference to it in one transaction, logging a renamed line to today’s daily note; or, with relink, keep the slug and rewrite the links still under a former name. (cdno note rename) |
The preview is the contract. The sequence an agent follows:
- Preview. Call with
dry_run: true. The result is the plan and nothing is written (written: false); a refusal here is exactly the refusal a write would get. - Show. Put the plan in front of the person: what moves, how many references in how many notes,
the
log_line, anywarnings. The person decides, not the agent. - Write. Call again with the same
note,toandreason(the reason is part of the plan) andexpected_planset to the preview’splan_hash. What is written is the plan that was shown; if the vault changed in between the call is refused withstale_plan(below) and writes nothing.
note takes the references read_note takes, including a former name of the note
(found_by_former_name in the result says which); reason is a short clause for the log line. A
rename to the note’s current slug is an empty plan: written: false, log_line: null.
Relinking. relink: true in place of to keeps the note’s slug and rewrites every link that
reaches it only through a former name, links and reference fields alike (the links are the
stragglers lint reports, which a sync merge can bring back), to the current slug. The
same preview-then-write sequence applies, with relink: true on both calls. Nothing moves and the
aliases stay listed: the result has moves: [] and new_slug equal to from, and the log_line
is relinked [[<slug>]] — <title> with an indented references: <n> line. With no straggler the
plan is empty and nothing is logged. An archived action note can be relinked, though not renamed.
A blank to beside relink: true reads as omitted. relink with to, either with reason, or neither to nor
relink, is refused as an invalid argument naming the field
(cdno note rename --relink).
The result is the plan, whether or not it was written: dry_run, target, from (the note’s
current slug), new_slug, found_by_former_name (the former name note matched, or null),
moves ({from, to}), edits (a path with its changes, each of kind links with a count,
field with a key, alias or log_line), references, notes, warnings (each with a kind
and a message: frozen_prefix_already_diverged, old_slug_already_an_alias, or sync_check),
plan_hash, log_line (null when the plan is empty), then written, index_stale (true: the
files are written and the rename succeeded; the index catches up on the next open) and
verification. A write nudges the sync agent only when the HTTP server runs with --sync-nudge (off by default; the stdio server never does); a preview never does.
Before planning, the tool checks the vault against its upstream as last fetched: it never fetches. A preview is refused exactly as a write would be.
Refusals (a rejection; nothing is written):
| Code | When |
|---|---|
stale_plan | The vault changed since the preview (any log append does it, and so does a different reason). details carries expected and actual. Preview again and show the person the new plan; never retry the same expected_plan. A blank expected_plan is refused as an invalid argument. |
behind_upstream | The vault is details.behind commits behind its upstream. Retry in a few seconds; if still behind, the sync is stalled: tell the person rather than set skip_sync_check. |
git_operation_in_progress | A merge, rebase or similar was left paused. Never overridable. Both git codes carry the check’s warnings in details when it had any. |
stem_shared | Another note shares the slug, so links to it cannot be told apart. |
note_not_renamable | The note is of a kind the rename does not take (details.reason: archived, folder, a type). A portfolio whose slug a question shares is renamed through the question. |
reference_drift / reference_unaddressable | A referrer’s frontmatter would change beyond the reference, or names the note in a form the rewrite cannot address; the person fixes the note by hand first. |
rename_incomplete | A write failed and the rollback could not restore details.paths: the person checks them by hand. |
slug_taken / invalid_slug | to is taken or malformed: refused, never suffixed. |
not_found / ambiguous_slug | The note reference matches no note, or several, as for read_note; a question and its same-slug portfolio resolve to the question. |
Frontmatter
| Tool | Inputs | Effect |
|---|---|---|
set_frontmatter | note, key, value | Set a declared, settable = true typed frontmatter field through the index (no desync). note is today, a YYYY-MM-DD date, or a vault-relative path. Engine-owned keys (type, status, a period key, and a project’s closed, owned by complete_project / drop_project / activate_project) are rejected; the value is type-checked; log_on_change fields stamp a daily-log line. (cdno frontmatter set) |
Daily, weekly, and monthly sections
| Tool | Inputs | Effect |
|---|---|---|
note_to_daily | heading, body, date? | Write worked-out substance to a daily note as one ### <heading> entry under ## Notes, with its pointer line in ## Logs, in one write. See below. (cdno log note) |
upsert_daily_section | section (Standup|Intention|Agenda|Meeting|Notes), content?, date?, append? | Write or append a daily-note section. Notes is append-only: it takes append: true only, and append: false is refused; for one entry with its ## Logs pointer, use note_to_daily. |
upsert_weekly_section | section (Wins|Challenges|One Improvement|This Week's Goal), content?, date?, append? | Write or append a weekly-note section. |
upsert_monthly_section | section (Wins|Themes|Next Month's Focus), content?, date?, append? | Write or append a monthly-note section. |
note_to_daily
| Input | Meaning |
|---|---|
heading | The entry’s heading, written as ### <heading> under ## Notes and used verbatim as the pointer’s anchor. |
body | The entry’s substance. Leading blank lines and trailing whitespace are dropped. Its wikilinks are copied onto the pointer line. |
date | ISO YYYY-MM-DD of the daily note to write to, stamped at the current time. Omitted = today. |
The entry is appended under the day’s ## Notes, which is created immediately above ## Logs if
the day has none (the daily note itself is created if absent). The pointer line
noted [[journal/<year>/daily/<date>#<heading>]], followed by the body’s wikilinks in parentheses
when it has any, is appended to ## Logs in the same write, so the entry is not logged again with
append_to_log. End an entry worth reusing with the tag #concept on its last line, so a later
review can find it as a candidate for promotion to a concept note.
The result adds target — the entry’s anchored link, journal/<year>/daily/<date>#<heading>, to
cite as [[<target>]] (for example from a concept’s origin) — and log_line, the pointer line
without its timestamp.
Refused with code history_entry_heading_invalid (nothing is written) when the heading reuses a
daily section name (Standup, Intention, Agenda, Meeting, Notes, Logs), matches a heading
already in that day’s note, contains [, ], |, # or inline markup (bold, italics, code),
or starts with ^. Headings inside the body must be level 3 or deeper and are held to the same
uniqueness rule, so a pasted derivation with its own ## Proof must be demoted first. A blank
heading or body is refused as an invalid argument (-32602).
Notes
append?defaults to replacing the section; set ittrueto append instead.- Dates are
YYYY-MM-DD; week-scoped tools accept any day in the target week, and month-scoped tools accept any day in the target month. vars?is an optionalname -> valuemap supplying values for a custom template’s[variables.prompt]placeholders — the MCP analogue of the CLI’s repeatable--var name=value. Omitting a required prompted variable fails with an “unresolved prompts” error. See Creation and lifecycle tools for the full list of templated tools that accept it.- See also: Creation and lifecycle tools, JSON output.
Creation and lifecycle tools
Tools that create new notes or move existing ones through their lifecycle.
Creation
| Tool | Inputs | Effect |
|---|---|---|
create_project | title, context, core_question?, slug?, vars? | Create a project (parked if at the active cap). (cdno project create) |
create_portfolio | question, project?, slug?, vars? | Create an evidence portfolio. question is free text or an existing question’s slug; given a slug, the portfolio takes that question’s text as its title, links both ways, and slug must be omitted or equal to it (portfolio_slug_mismatch otherwise). |
create_question | domain (research|life), text, slug?, vars? | Create a question note. |
create_stewardship | name, context, expanded?, slug?, vars? | Create a stewardship; expanded adds a tracking/ folder. |
create_custom_note | type_name, title, fields?, slug?, vars?, body?, origin? | Create a note of a config-defined custom type ([note_types.<name>]); built-in types have their own dedicated create tools, on this page and under Write tools. fields is a name → value map of the type’s declared frontmatter fields — every required one must be present, and each key must be declared. body excludes the title heading (the engine writes the H1) and fills the template’s {{body}} placeholder, or is inserted after the H1; origin is one string of wikilinks to where the note came from, always stored as a frontmatter string, and is refused unless the type declares an origin field. Creation is logged to today’s daily note as <type> created [[<folder>/<slug>]] — <title>; a promotion (creation with origin) needs no other log line. Pass slug to name the file; passing it beside a slug entry in fields is refused (slug_given_twice). Without slug, a second note with the same title is not refused but created as <slug>-2, so for a concept, search the library (search_notes with note_type: concept) and refine an existing note with revise_note instead. Call list_note_types first to discover a vault’s types and their fields. (cdno note create) |
link_portfolio_to_question | portfolio, question | Retrofit a portfolio→question link (backlinks both ways). |
link_portfolio_to_project | portfolio, project | Retrofit a portfolio→project link (sets project: and appends to the project’s Links). |
Lifecycle
| Tool | Inputs | Effect |
|---|---|---|
park_project | project | Move an active project to _parked/. |
complete_project | project | Close an active or parked project whose work is done: move it to _done/<year>/ with status: completed and closed:. Refused while any action or milestone is open. (cdno project complete) |
drop_project | project, reason?, open_items? (refuse|drop), expected_open_items? | Close an active or parked project that is not going to happen: status: dropped, closed:, moved to _done/<year>/. Refused with the open items by default; open_items: "drop" with the refusal’s hash drops them too. (cdno project drop) |
activate_project | project | Bring a parked or closed project back, clearing closed: (enforces the five-project cap). |
set_question_status | question, status (active|parked|answered|retired) | Transition a question’s status. |
add_periodic_commitment | stewardship, title, recurrence, next_date | Append a periodic commitment to a stewardship dashboard. |
The holder
| Tool | Inputs | Effect |
|---|---|---|
hand_off_project | project, holder?, take_back? | Record whose move an open (active or parked) project is. Exactly one of holder (the name the person gives, written as held_by: "<name>") or take_back: true (writes held_by: null); omitting both is an error, never a silent take-back. The map keeps its status and folder. Logs holder on [[<slug>]] with indented was: / now: lines, (mine) for no holder; naming the holder it already has changes nothing. (cdno project hand-off, take-back) |
An active project that names a holder does not count toward the cap (counts: false, and
slots.counted on list_projects leaves it out). Handing off is never
the way to make room at the cap: call it only on the person’s word that someone else is moving the
project. Rejections:
empty_field(remedy: take_back): an emptyholder, or one on the vault’s[projects] holder_sentinels(a name meaning the person or no one in particular). If they cannot name who has it, it is theirs.project_in_focus: an action on the project is in focus; the person pauses it first.project_cap_reached: taking back an active project would make it count with the counted projects already at the cap; itsdetailscarrycurrent,maxandactive_projects, which lists only the projects that count. Taking back a parked project only clears the name and never activates it.project_not_active: the project is completed or dropped.
The tool also appears with the other project writes under Write tools.
Naming a note
The eight create tools (create_project, create_question, create_portfolio, create_stewardship,
create_custom_note, add_action, promote_action, create_commitment) take an optional slug
naming the new note’s file; omitted, it is derived from the title. Propose one when the title
is long. It follows the slug rule: lowercase a-z, 0-9 and single -, at most 50 characters, not
something YAML reads as a number, boolean, null or date. An explicit slug is used exactly or refused,
never suffixed with -2. Refusals are coded rejections with nothing written: invalid_slug,
slug_taken, portfolio_slug_mismatch and slug_given_twice, listed with their details under
When a call is rejected. add_action ignores slug unless with_note
is true: an inline bullet has no file.
Notes
contextis one of the fixed life domains.recurrencefollows the recurrence syntax:daily,weekly,monthly,yearly, orevery N months.activate_projectenforces the cap — if activating would make more projects count than the cap (active and naming no holder), the call fails and the assistant must park one first.- Closing a project never needs a slot, and works on a parked project directly. While an action or
milestone is open, both closing tools return a
project_has_open_itemsrejection whosedetailslist theactions, themilestones(withdateandhard), theuntouched_commitments(linked standalone commitments, which no close ever touches) and anopen_items_hash.complete_projectstays refused until each item is completed or dropped: a completion is a claim that the work was done.drop_projectwithopen_items: "drop"andexpected_open_itemsset to that hash drops every listed item with the project, each logged withreason: project dropped (<reason>)(plainreason: project droppedwhen noreasonwas given); if the list has changed since, it is refused again with the new list and hash. The result namesdropped_actions,dropped_milestonesanduntouched_commitments. - A closed project can take the other outcome later (
complete_projecton a dropped one, and the reverse), re-filed under this year’s folder with a newclosed:date. The same outcome again is aproject_not_activerejection whosedetailscarrystatusandclosed. vars?is an optionalname -> valuemap supplying values for a custom template’s[variables.prompt]placeholders — the MCP analogue of the CLI’s repeatable--var name=value. Supply an entry for each prompted variable the note’s template uses that has no static[variables]default; otherwise creation fails with an “unresolved prompts” error (MCP has no interactive prompt to fall back on).- See also: Write tools, Context-gathering tools.
Using with Claude skills
The MCP tools are building blocks. A skill composes them into a repeatable ritual the assistant can run on request — “do my daily orientation,” “walk me through the weekly review.” Cuaderno ships worked examples you can adapt.
Where the examples live
The repo’s examples/skills/
directory contains three reference skills:
quick-capture— capture a thought into the inbox with minimal friction.daily-orientation— read your orientation and help set the day’s intention.weekly-review— celebrate the week’s wins first, scan stewardship habits, walk the projects, look two weeks ahead, and pick one goal for next week, written to the weekly notes.
Each is a directory with a SKILL.md (YAML frontmatter + a Markdown body of steps), plus shared
references/. The README there documents the authoring pattern.
The pattern
A skill typically:
- Surfaces context with a read tool — e.g.
get_orientationorget_weekly_context. - Talks it through with you, deciding what to do.
- Writes with the matching tool — e.g.
append_to_log,upsert_daily_section,update_project_state.
For example, a daily orientation skill calls get_orientation, presents the commitments and the
suggested start, asks for your intention, and writes it with
upsert_daily_section(section="Intention", ...).
A weekly review skill calls get_weekly_context, whose projects carry the
review fields, and asks each project the question its relationship
implies rather than one question of all of them:
| The project | The question |
|---|---|
Yours (counts: true) | Did it move? |
Handed off, and changed_this_week | What came back, and what is waiting on your decision? |
Handed off over an empty Waiting On (waiting_on_empty) | A name over an empty Waiting On is a parked map wearing a name: park it, or take it back? |
Handed off, with focus_days_while_held of two or more | Offered as evidence: take it back, or change the name to whoever really has it? |
The skill asks and never concludes: the fields are evidence, and the person decides. It never offers a hand-off as a way to make room at the cap. See Weekly review.
Graceful degradation
Because each step maps to a discrete tool, a skill can degrade gracefully — if a write tool isn’t available or you decline it, the read half still gives you the briefing. The example skills show how to bind steps to tools and reference shared material.
Build your own
Start from an example, swap in the tools your ritual needs (see reads, writes, creation & lifecycle), and keep each step mapped to one tool so the flow stays inspectable. Then install it like any other Claude skill.
See also
- Connect to Claude — register the server.
- MCP server reference — the full tool surface.
JSON output
Adding --json to a supported verb swaps the formatted table for machine-readable JSON. These shapes
match the MCP server DTOs, with the exceptions noted below, so you get the same structures from the CLI and from an
AI client. (Which verbs support --json is covered in the CLI overview.)
Write verbs → a result object
Every write verb emits the same small object and runs non-interactively (project complete and
project drop add to it; see below):
{
"path": "projects/surrogate-model.md",
"message": "Created projects/surrogate-model.md"
}
path is the vault-relative file written or updated; message is the human-readable line. For the
two-file cases (e.g. action add --note), path is the file the verb considers primary.
project complete / drop --json → a result or a refusal
A close that goes ahead emits the result object with three more arrays: what the drop let go, and the linked standalone commitments, which no close ever touches.
{
"path": "projects/_done/2026/alpha-study.md",
"message": "Dropped alpha-study. 0 of 5 slots in use. Let go: 1 action, 1 milestone.",
"dropped_actions": ["Define first concrete step (light)"],
"dropped_milestones": ["Submit"],
"untouched_commitments": []
}
A close refused because actions or milestones are open prints the rejection object the MCP server
returns, on stdout, and exits 1:
{
"code": "project_has_open_items",
"message": "project 'alpha-study' has 2 open item(s) — tick the ones that are done, or let them go explicitly",
"details": {
"slug": "alpha-study",
"actions": [{ "text": "Define first concrete step (light)", "note": null, "note_status": null }],
"milestones": [{ "title": "Submit", "date": "2026-12-01", "hard": false }],
"untouched_commitments": [],
"open_items_hash": "76d0efb83a540bf9"
}
}
Slug refusals
An explicit --slug that is refused prints the rejection object on stdout and exits 1, on any of the
eight create verbs (the slug rule). The shape is the one the
MCP server returns, and details depends on the code:
code | details | When |
|---|---|---|
invalid_slug | slug, why | The slug breaks the shape rule; why says which part. |
slug_taken | slug, holder, kind | Another note already holds the name. holder is that note’s path; kind is stem (its filename), folder (the folder of a folder note) or alias (a former name another note still lists). |
portfolio_slug_mismatch | question_slug, slug | portfolio create --question <question-slug> with an explicit --slug that is not that question’s slug. |
slug_given_twice | field | note create given --slug beside --field slug=…; field is slug. |
cdno portfolio create --question "Sparse vs dense attention OOD" --slug surrogate-model --json
{
"code": "slug_taken",
"details": {
"holder": "projects/surrogate-model.md",
"kind": "stem",
"slug": "surrogate-model"
},
"message": "slug 'surrogate-model' is taken by 'projects/surrogate-model.md' (as its stem)"
}
Nothing is written on a refusal. Do not retry with a -2 of your own; ask for another name.
Rename refusals
cdno note rename --json prints the plan on success (see note rename):
{
"dry_run": false,
"target": "projects/old.md",
"from": "old",
"new_slug": "surrogate-model",
"found_by_former_name": null,
"moves": [{ "from": "projects/old.md", "to": "projects/surrogate-model.md" }],
"edits": [
{ "path": "journal/2026/daily/2026-11-06.md", "changes": [{ "kind": "links", "count": 2 }, { "kind": "log_line" }] },
{ "path": "projects/old.md", "changes": [{ "kind": "alias" }] }
],
"references": 2,
"notes": 1,
"log_line": "renamed [[surrogate-model]] — Old\n was: old",
"warnings": [],
"plan_hash": "5c380833154ebb71",
"written": true,
"index_stale": false
}
| Field | Meaning |
|---|---|
dry_run | true for a preview. |
target, from, new_slug | The note’s path and current slug before the rename, and the slug it takes. |
found_by_former_name | The former name the selector matched, or null. |
moves | {from, to} pairs; a portfolio’s folder, or a question and its portfolio, move together. |
edits | One {path, changes} per rewritten path, in path order; see changes below. |
references, notes | References rewritten (links plus fields) and the notes holding them. |
warnings | {kind, message, …} entries that do not stop the plan; kinds below. |
plan_hash | An opaque digest of the plan; the MCP tool takes it back as expected_plan. |
log_line | The renamed entry without its time stamp, or null for an empty plan. |
written, index_stale | Whether anything was written; whether the index failed to follow (files are correct, the next open reconciles). |
verification | MCP only: the read-back proof, null unless a write landed. |
written is whether anything was written (false on --dry-run and for a rename to the current slug); log_line is the unstamped renamed entry appended to today’s daily note. Under --relink the shape is the same, with moves empty, new_slug equal to from, written false when there was no straggler, and log_line the relinked entry. The MCP rename_note tool returns the same object plus verification. A warnings entry is {kind, message, …}: frozen_prefix_already_diverged adds path (an archived referrer that already failed its archival check, rewritten anyway), old_slug_already_an_alias adds holder (the old slug is already another note’s alias, so it will resolve to nothing once shared), and sync_check is a warning of the sync check (a failed fetch, no upstream, a state git could not report, an override that let a behind vault through), on a dry run and a write alike. A changes entry is links (with count), field (with key), alias or log_line. A refusal
prints the usual {code, message, details} object and exits 1, writing nothing:
code | details | When |
|---|---|---|
invalid_slug, slug_taken | as above | The new slug breaks the rule, or another note holds it. |
note_not_renamable | path, reason | reason is type <name>, archived, folder, rename the question, or what stops the alias being recorded. |
stem_shared | slug, path, others | Another note has the same file stem, so links to it cannot be told apart. |
stale_plan | expected, actual | The vault changed between the preview and the write. Preview again. |
reference_drift | path, drift | Rewriting the references in path would change more than them. |
reference_unaddressable | path, key | A reference field names the note in a form the rewrite cannot address. |
rename_incomplete | paths, source | A write failed and the rollback could not restore the paths in paths; check them by hand. |
behind_upstream | behind, remedy | The vault is behind commits behind its upstream (after a fetch; for a dry run, as last fetched). remedy says what to do: sync first, or pass --skip-sync-check. |
git_operation_in_progress | reason | A git operation is paused mid-way (reason names it, e.g. .git/MERGE_HEAD present). Not overridable: finish or abort it first. |
search --json → an array of hits
Ranked best-first; each hit:
[
{
"path": "portfolios/sparse-vs-dense-attention-ood/2026-03-15-chen-2025.md",
"note_type": "evidence",
"title": "Chen et al. 2025",
"snippet": "...4x speedup at 95% accuracy on the OOD split...",
"score": 1.7
}
]
A lower score ranks earlier (best match first). No matches → [].
open --json → the resolved path
cdno open today --json
{ "path": "/home/you/vault/journal/2026/daily/2026-08-21.md" }
Absolute, so the value composes from any directory. A reference that resolves to nothing — or to more than one note — is an error, not an empty result.
open --list --json → an array of candidates
Every note in the vault, most-recently-modified first. The plain-text form of the same listing is
tab-separated for piping into a fuzzy finder; --json is for programmatic consumers.
[
{
"path": "projects/surrogate-model.md",
"title": "Surrogate model",
"type": "project"
}
]
title is the note’s body H1, and is null for a note that has none.
list verbs → arrays of summaries
cdno project list --json
[
{
"slug": "surrogate-model",
"status": "active",
"state_snippet": "Mesh scaling works; assembly is the bottleneck",
"top_action": { "text": "Profile the assembly step", "energy": "medium" }
}
]
project list --closed→[{ "slug", "title", "context", "outcome", "closed_on" }], most recently closed first;outcomeis the closure,completedordropped, and never a parent. This row is the CLI’s own: MCPlist_projectswithinclude_closedreturns closed maps as{ "slug", "frontmatter" }.portfolio list→[{ "slug", "question", "evidence_count", "last_updated", "staleness_days" }]stewardship list→[{ "slug", "name", "context", "variant", "tracking_count" }]action list→[{ "text", "energy", "attached": { "slug", "status" } | null }]
show verbs → a detail object
cdno project show surrogate-model --json # same shape as a project-list element
cdno portfolio show --portfolio sparse-vs-dense-attention-ood --json
{
"slug": "sparse-vs-dense-attention-ood",
"question": "Sparse vs dense attention OOD",
"created": "2026-03-01",
"project": "[[projects/surrogate-model]]",
"evidence": [
{
"path": "portfolios/sparse-vs-dense-attention-ood/2026-03-15-chen-2025.md",
"created": "2026-03-15",
"source": "Chen et al. 2025",
"origin": "[[projects/surrogate-model]]"
}
]
}
projectisnullwhen the portfolio isn’t linked to one.- An evidence entry gains a
"kind"field (e.g."pdf") only for attachment stubs; it’s omitted for plain prose notes. stewardship show→{ "slug", "name", "context", "variant", "body_markdown" }.
orient --json → the orientation object
cdno orient --json
{
"commitments": [],
"projects": [
{
"slug": "surrogate-model",
"status": "active",
"context": "work",
"core_question": "[[questions/research/surrogate-generalises]]",
"state_snippet": "Mesh scaling works; assembly is the bottleneck",
"top_action": { "text": "Profile the assembly step", "energy": "medium" },
"held_by": null,
"counts": true,
"waiting_on_empty": true
},
{
"slug": "mesh-study",
"status": "active",
"context": "work",
"core_question": "[[surrogate-generalises]]",
"state_snippet": "Coarse-mesh run validated end to end",
"top_action": { "text": "Write up the convergence plot", "energy": "light" },
"held_by": "the modelling group",
"counts": false,
"waiting_on_empty": false
}
],
"groups": [
{
"parent": { "kind": "question", "path": "questions/research/surrogate-generalises.md" },
"projects": ["surrogate-model", "mesh-study"]
}
],
"slots": { "active": 2, "counted": 1, "cap": 5, "counted_slugs": ["surrogate-model"] },
"lapsed_habits": []
}
projects holds one project-list element per active project; each carries core_question, the
map’s frontmatter value exactly as written, or null when it sets none (project list and
project show elements carry it too). Each also carries three keys about the cap (project list
and project show elements too): held_by is the name of whoever else is moving the map, trimmed,
only when the map’s held_by: names someone else, and null when the map is yours (no held_by:,
or me, tbd or another entry of holder_sentinels) or the value is
unreadable (not a plain string); counts is true when the map holds one of the
max_active_projects slots, that is, it is active and does not name someone else as its holder
(branch on counts, never on the name); and waiting_on_empty is true when the map’s ## Waiting On is missing, empty, or only
(nothing yet). MCP get_orientation projects, get_weekly_context.projects and
get_monthly_context’s parked and stuck entries carry the same three keys; the weekly ones add
changed_this_week and focus_days_while_held (see the
review fields). groups arranges the same projects, by slug, under the
core question they share: a short and a full link to one question group together, since the key is
the question’s vault path (or the link’s raw target when it resolves to no note), and only the
field counts, never a passing mention of the question in a map’s body. Every project falls in
exactly one group, groups follow the order of their first member, and a project with no question
is a group of its own with "parent": null. MCP get_orientation carries groups in the same
shape. slots is the active projects
against the cap: active counts the maps with status: active, counted those holding a slot
(the active maps whose counts is true), which is the number the cap is checked against, cap is the configured max_active_projects, and
counted_slugs names the counted maps. MCP get_orientation carries slots as
{ "active", "cap" }, the object list_projects returns.
Casing
Enumerations serialise in their canonical lowercase/kebab form, matching the MCP DTOs:
status → active/parked/completed/dropped, energy → deep/medium/light, stewardship variant →
flat/expanded, context → work/side-project/household/… So a value is identical whether you
read it from cdno --json or over MCP.
Colour and interactivity
cdno’s human-readable output is coloured, and several read commands offer to open one of the rows
they just printed. Both behaviours are for people at a terminal, and both switch themselves off the
moment the output is going somewhere else.
Cards
Listings whose items carry prose — project list, portfolio list, stewardship list, questions,
search, and the active-projects section of orient — render as cards: a coloured bar down the left
of every line an item owns, the identifier as a title, a badge aligned into a shared column, and the
text wrapped underneath.
3 active projects
▎ surrogate side-project
▎ Six contributors settled; scope is fixed to the solver rather than the
▎ mesher, and the validation plan is agreed.
▎ next: Draft the validation plan
▎ mesh work
▎ Coarse-mesh run validated end to end; two workstreams in flight.
▎ next: Profile the assembly step
What the bar’s colour means depends on the listing, and it is always the thing that listing exists to
surface: context for project list and orient, staleness for portfolio list and
stewardship list (amber once nothing has been filed for a month), and domain for questions.
search colours every hit alike — relevance is already the ordering.
Commands whose rows are genuinely tabular keep their tables — status, commitments, orient’s
commitments section, and a portfolio’s evidence list all have short, aligned fields where a column
beats a card. show commands keep their plain line shape too: a bar earns its space by marking where
one item ends and the next begins, and a detail view has only one item.
note list is untouched. It prints one bare path per line because it exists to be piped into other
tools, and a header or a gutter would break that.
Colour
| Setting | Effect |
|---|---|
--color auto | Default. Colour only when stdout is a terminal. |
--color always | Colour even when redirected — for cdno project list --color always | less -R. |
--color never | Never colour. |
--colour is accepted as a spelling of the same flag.
Under auto, three environment variables are consulted, in this order:
NO_COLOR(set and non-empty) turns colour off.CLICOLOR=0turns colour off.CLICOLOR_FORCE(set and non-empty) turns colour on even when redirected.
NO_COLOR deliberately outranks CLICOLOR_FORCE: NO_COLOR is something you export once as a
preference about your own terminal, while CLICOLOR_FORCE is usually set by a harness that has no
standing to override it. An explicit --color outranks all three.
--json output is never coloured, whatever any of the above says. Scripts can pass --color always
safely.
Plain output
cdno open is deliberately outside all of the above. It prints one absolute path, and --list
prints one tab-separated row per note — no cards, no colour, no alignment, whatever the terminal
is. Both are written to be consumed by another program ($(…), fzf, cut), and a colour escape
or a padded column would corrupt them. The tab is load-bearing: it is what fzf --delimiter='\t'
splits on.
Interactive reports
In a terminal, project list, portfolio list, stewardship list, orient, status, and
search follow their output with a picker:
? Inspect a project
❯ surrogate (side-project)
mesh (work)
garden (family)
[↑↓ to move, enter to inspect, Esc to leave]
Choosing a row prints exactly what the matching show command would print, then asks again. Esc or
Ctrl-C leaves, with exit status 0.
search is the exception, and deliberately: choosing a hit opens it in your editor and the
command ends there, rather than returning to the list. Once an editor has the file, coming back to
the search results is not what anyone wants. cdno open’s own picker behaves the
same way.
The listing is always printed first, so the prompt only ever adds to what you would have seen. It is skipped entirely when:
- either stdin or stdout is not a terminal — piped, redirected,
< /dev/null, a background job, or running under CI, which is the usual reason neither end is a terminal, --no-interactiveis passed,--jsonis passed, or- the terminal is narrower than 20 columns, which is too narrow to draw a picker in.
Both streams matter: the listing is written to stdout but the picker reads stdin, so a caller with a terminal on only one end is not offered the prompt. There is no separate CI detection — a CI job has no terminal, and that is what actually decides it.
That makes the same command safe for a person, a shell pipeline, and an AI agent without changing anything about how it is invoked.
Width
On a terminal, output is laid out to the terminal’s width as measured when the command runs. Text with
nowhere to break — a long URL, a long slug, or Thai and Lao, which need dictionary-based word
segmentation cdno does not yet do — runs past the edge rather than being cut, on the grounds that a
path you can copy beats a path that fits. cdno
prints once and exits, so resizing afterwards does not reflow anything already on screen — run the
command again. Everywhere else — piped, redirected, or when the terminal reports no usable size (a
pty opened without one reports zero columns) — it lays out to a fixed 100 columns, so captured output
is deterministic and diffable.
Text from your notes is sanitised before it is laid out — in listings and in show views alike, and
in card titles as well as bodies. Tabs and carriage returns become spaces; other control characters,
including escape sequences, are replaced. A note is data, and without this a stray escape in a note
could repaint the terminal, move the cursor, or draw over the card’s own gutter. The raw markdown is
of course untouched, and cdno note still prints paths verbatim.
Frontmatter fields
Every note begins with a YAML frontmatter block. Cuaderno parses it into a typed structure — if it
parses, it’s valid (see Business rules). This page lists the fields
per note type. ? marks an optional field.
Notes that
cdnocreates are already well-formed. You mainly need this when hand-authoring or migrating notes — andcdno normalisewill reorder keys to the canonical order for you.
daily
type: daily
date: 2026-04-25
tags: [] # auto-populated
weekly
type: weekly
week: 2026-W17
date_start: 2026-04-20
date_end: 2026-04-26
monthly
type: monthly
month: 2026-04
date_start: 2026-04-01
date_end: 2026-04-30
project
type: project
context: work # work | side-project | university | family | household | legal | personal
status: active # active | parked | completed | dropped
created: 2026-04-25
core_question?: "[[questions/research/surrogate-cost]]"
held_by?: "the harbour office" # who else is moving it; set by hand-off, cleared by take-back
closed: null # set by complete/drop, cleared by activate
status and closed are owned by the lifecycle verbs (park, activate, complete, drop), so
cdno frontmatter set refuses both. status has to match the folder the map lives in: projects/
for active, projects/_parked/ for parked, and projects/_done/<year>/ for completed or
dropped. A map written before closed: existed needs no edit, since a missing closed: reads as
open.
held_by (optional, on an active or parked map) names whoever else is moving the project. An active
map that names a holder does not count toward the
cap; the map keeps its status and folder.
cdno project hand-off writes it quoted and
take-back writes held_by: null, each logging
holder on [[<slug>]], so cdno frontmatter set refuses it. When you edit it by hand:
- A missing line,
null, a blank value, or a value on[projects] holder_sentinels(me,tbd,someone, …) means no holder: the map is yours and counts.cdno lintwarns on a sentinel or blank value. - A value that is not a plain string (an unquoted
[[people/ana]], which YAML reads as a list, a number, a date) also means no holder, andcdno lintreports it as an error. Quote a wikilink:held_by: "[[people/ana]]".
action
type: action
status: active # active | completed | blocked | dropped
project: surrogate-model
energy: deep # deep | medium | light
milestone?: "[[...]]"
due?: 2026-05-10
criteria?: "Definition of done"
blocker?: "What it's waiting on"
created: 2026-04-25
completed?: 2026-05-01
tags?: []
portfolio
type: portfolio
question: "Sparse vs dense attention OOD"
created: 2026-03-01
project?: "[[projects/surrogate-model]]"
evidence
type: evidence
created: 2026-03-15
source: "Chen et al. 2025"
portfolio: sparse-vs-dense-attention-ood
origin: "[[projects/surrogate-model]]"
kind?: pdf # only for attachment stubs (pdf | image | video | …)
stewardship
type: stewardship
context: personal # one of the life-domain contexts
tracking
type: tracking
stewardship: health
activity: gym
date: 2026-04-06
duration_min?: 60
routine?: "[[stewardships/health/routines/upper-body-a]]"
question
type: question
domain: research # research | life
status: active # active | parked | answered | retired
created: 2026-04-25
updated?: 2026-05-01
commitment
type: commitment
status: active # active | completed | dropped
due: 2026-06-01
created: 2026-04-25
completed?: 2026-06-01
context: personal
project?: icml-paper
stewardship?: finances
Extending schemas
You can require additional fields per type via config.toml ([schemas.<type>] extra_required) —
see Configuration reference. Required built-in fields are always enforced on top.
Configuration reference
The vault’s settings live in .cuaderno/config.toml, written by cdno init. Every key is optional —
defaults are applied when omitted. For the conceptual tour, see
Configuration.
Full example
[vault]
name = "My Research Vault"
method_version = "1.2" # the RLM version; written by init, changed by `cdno migrate`
max_active_projects = 5 # the active-project cap
# Glob patterns excluded from the index (search, lint, link checks).
# Additive only — no "!" negation. Matched against vault-relative paths.
# NEVER deletes files; only scopes what the index considers.
ignore = ["CLAUDE.md", "README.md"]
# Per-type extra required frontmatter fields. Built-in required fields
# are always enforced; these add vault-specific requirements (cdno lint).
[schemas.project]
extra_required = ["collaborators"]
[schemas.evidence]
extra_required = []
# Typed frontmatter fields for a built-in type. Recognised by the
# Templates editor and type-checked by `cdno lint`.
[schemas.daily.fields.meds]
type = "bool" # bool | int | float | string | date
default = false # static, type-checked against `type`
[schemas.daily.fields.mood]
type = "string"
values = ["low", "ok", "good"] # allowed values (a string constraint)
default = "ok"
# How an activity's tracked numbers are read back. Each metric declares how
# it collapses to one point per date; without a declaration the activity's
# series come from its body table, with each column summed.
[tracking.practice]
records = "detail" # frontmatter key holding repeated records
group_by = "subject" # one series per distinct value
[tracking.practice.metrics.minutes]
aggregate = "sum" # a TOTAL
unit = "min"
[tracking.practice.metrics.focus]
aggregate = "mean" # a RATING; sum would grow with how often you log
# Static template variables — resolve in any custom template ({{author}}).
[variables]
author = "A. Researcher"
# Prompted variables — gathered at note creation (--var, prompt, or error).
[variables.prompt]
collaborators = "Who are the collaborators?"
Keys
| Key | Type | Default | Purpose |
|---|---|---|---|
vault.name | string | "My Vault" | A human label for the vault. |
vault.method_version | string "<major>.<minor>" | absent (read as 1.0) | The version of the Research Logbook Method the vault follows, not cdno’s version. Written by cdno init; changed only by cdno migrate. See Method version. |
vault.max_active_projects | integer | 5 | The active-project cap: how many projects may count, that is, be active and name no one else in held_by:. |
ignore | list of globs | [] | Files the index skips. Additive; never deletes. See Ignore globs. |
schemas.<type>.extra_required | list of strings | [] | Extra required frontmatter fields for a built-in note type (not enforced on a custom type), enforced by cdno lint. |
schemas.<type>.fields.<name> | table | — | A typed frontmatter field for any note type, built-in or custom (type, default, required, values, settable, log_on_change). Recognised by the Templates editor (built-in types), type-checked by cdno lint, and (when settable) writable via cdno frontmatter set. See Typed schema fields. |
tracking.<activity> | table | — | Declares how an activity’s tracked numbers are read back (records, group_by, and a metrics.<name> table per metric). Without one, the activity’s series come from its body table with each column summed. See Tracking. |
focus.carry_over_days | integer | 1 | How many days back the focus is read from today. 0 reads today only. See Focus. |
focus.paused_lookback_days | integer | 14 | How far back a pause is still offered for resume, cdno now and orientation. |
focus.max_window_days | integer | 366 | Ceiling for the two windows above; a vault that exceeds it fails to open. |
projects.holder_sentinels | list of strings | ["me", "I", "myself", "(mine)", "someone", "somebody", "nobody", "unknown", "tbd", "n/a", "none", "-", "?"] | Values of a project map’s held_by: that name you or nobody, so they read as no holder. Trimmed, any case; [] accepts any name. See Projects. |
note_types.<name> | table | — | Declares a config-defined custom note type (folder, required/optional fields, template, …) — a schema-only type for entities the built-ins don’t cover. See Custom note types. |
variables.<name> | string | — | Static template variable; resolves in any custom template (per-type values win on name clash). |
variables.prompt.<name> | string | — | Prompted template variable; the value is the prompt text. Gathered at creation from --var name=value, an interactive prompt, or a static [variables] default; errors if none supplies it. |
Method version
[vault] method_version records the version of the
Research Logbook Method the vault follows, as two numbers, "1.1": no patch
part and no v. It is the method’s version, not the crate version cdno is released under, and it
changes only when the method’s text does.
[vault]
method_version = "1.1"
cdno initwrites the version this cdno implements. A vault with no key (one created before the key existed) is unstamped and read as RLM 1.0; nothing breaks.- Only
cdno migratechanges it: runcdno migrate --dry-runto see what it changes, thencdno migrate. Do not edit it by hand, since a migration’s file steps and its log entry go with the stamp. It is never changed when a vault opens, and never over MCP. - Every write re-reads the key under the write lock. A vault on an older major version than
this cdno is refused until you migrate it; a vault on a newer major is refused until you
upgrade cdno. Reads keep working in both cases. A different minor version writes normally;
cdno prints a one-line notice on stderr and
cdno lintmentions a newer one. - A malformed value (
"v1","1.1.0") is an error naming the key. - The key stays in
[vault], which older binaries read without rejecting unknown keys, so a vault stamped by a newer cdno still opens in an older one.
See Troubleshooting.
Ignore globs
ignore lists files that live in the vault directory but are not notes — repo scaffolding like
CLAUDE.md or README.md. They are excluded from the index, and therefore from search, lint and
backlinks as well. The files are never touched on disk.
Patterns are matched against each file’s vault-relative path:
| Pattern | Matches |
|---|---|
CLAUDE.md | that file at the vault root |
**/*.draft.md | a .draft.md at any depth |
folder/*/** | everything one or more levels below folder/<anything>/ |
folder/*/*/** | everything two or more levels below folder/<anything>/ |
The last two are the trap worth knowing. * stays inside one path segment but ** is recursive, so
portfolios/*/** does not mean “the level below a portfolio” — it matches the portfolio’s own notes
as well as anything nested under them. A glob written that way excludes every note in the folder it
was meant to tidy, and because an unindexed note is also unsearchable and unlinkable, the symptom
looks like a broken view rather than a misconfigured vault.
One thing guards against that: cdno reindex prints how many files the globs excluded, so an
over-broad pattern shows up as a number rather than as notes that quietly stopped appearing.
If notes go missing, clear the pattern and run cdno reindex: every row comes back.
Note that attachment artefacts filed into a portfolio need no ignore entry — they are excluded
automatically, by location. See vault structure.
Typed schema fields
[schemas.<type>.fields.<name>] declares a typed frontmatter field for any note type, built-in or
custom. It is the richer sibling of extra_required: instead of just a name, each field carries a
type (and optionally a default and an allowed-value set). Four things consume it today:
cdno templatesrecognises the field (built-in types only; a custom type’s placeholders are itsrequiredandoptionalfields, so declare the field there too), so a custom template referencing{{<name>}}no longer warns “renders literally”;- note creation populates the field’s
defaultat create — a custom template referencing{{<name>}}renders that default (a field with no default rendersnull), so the value lands in the new note’s frontmatter instead of a literal{{<name>}}(built-in types only; a custom type’sdefaultis not written — see Custom note types); cdno linttype-checks the field — a note whose value doesn’t match the declared type (or isn’t one ofvalues) gets a warning;- the
set_frontmattersetter (cdno frontmatter set, MCPset_frontmatter) writes the field through the index when it is markedsettable = true— see thecdno frontmatterreference.
[schemas.daily.fields.meds]
type = "bool" # bool | int | float | string | date
default = false # optional; static, type-checked against `type`
settable = true # optional; allow `set_frontmatter` to write it (default false)
log_on_change = true # optional; stamp a daily-log line when it changes
[schemas.daily.fields.mood]
type = "string"
values = ["low", "ok", "good"] # optional; allowed values (only valid on a string)
default = "ok"
required = false # optional; default false
| Field key | Type | Default | Purpose |
|---|---|---|---|
type | "bool" | "int" | "float" | "string" | "date" | (required) | The field’s scalar type. An unknown value is a hard load error. |
default | matching type | — | A static default value, type-checked at load. Populated at create when a custom template references {{<name>}}. A date is a quoted "YYYY-MM-DD". |
required | bool | false | Reserved for create-time enforcement (a later release); parsed now, but inert — it does not yet block creation. |
values | list of strings | — | An allowed-value constraint. Valid only on a string field. |
settable | bool | false | Whether set_frontmatter (cdno frontmatter set, MCP set_frontmatter) may write this field. Default-deny: absent or false means not settable. Never overrides an engine-owned key (type, status, a period key, and a project’s closed) — those stay blocked regardless. |
log_on_change | bool | false | When a settable field’s value actually changes, stamp a key: old → new line into today’s daily note in the same commit. |
Notes and limits:
- Defaults are static — there is no
"today"token; adatedefault is a literal calendar date. - A field only lands in frontmatter if a custom template references it — rendering substitutes
the
{{<name>}}tokens a template contains; it never adds a frontmatter line. The shipped built-in templates can’t reference vault-specific fields, so populate a declared field by adding a custom.cuaderno/templates/<type>.mdthat references{{<name>}}. - A create-path value wins over a declared default — if the note type’s create path already
supplies a value for that name (an engine-supplied placeholder), that value takes precedence and
the declared default does not apply. Likewise a
[variables]static var of the same name wins over a schema default. - A
[variables.prompt]name is owned by the prompt — if a field name is also a prompted variable, its value is collected via the prompt (from--var, an interactive prompt, or a static default), and the schema default is not used. This ensures a supplied answer is never discarded. - No
enumtype — model a closed set as astringwithvalues. intandfloatare distinct —intrejects anything with a fractional part, so a currency amount, a measurement or a rate wantsfloat. Afloatfield accepts a whole number too (82and82.5both validate), because a round reading is written without a decimal point. A non-finite default (nan,inf) is a load error — those values have no meaning as data.- List fields are reserved but not yet implemented — a
list = trueis a load error today. - Engine-owned keys are protected — you can’t declare a field named
type, or a calendar type’s own period key (daily→date,weekly→week,monthly→month); the vault refuses to open.set_frontmatteradditionally refuses to writestatusfor every type — even if a vault declares astatusfieldsettable = true— so the lifecycle commands stay its sole writers. extra_requiredstill works and is equivalent to an untyped, non-requiredstringfield; on a name clash an explicitfieldsblock wins.- A malformed field declaration (unknown
type, a mistyped key,valueson a non-string, adefaultthat doesn’t type-check) fails at vault-open, like every other config error.
Tracking
[tracking.<activity>] declares how an activity’s numbers are read back. Without one, a tracking
note’s series come from the first table in its body, with each column summed — right for a rep
sheet, wrong for a balance or a rating. Declaring an activity moves it to frontmatter, where each
metric says how it collapses.
[tracking.practice]
records = "detail" # frontmatter key holding repeated records
group_by = "subject" # one series per distinct value of this field
[tracking.practice.metrics.minutes]
type = "int"
aggregate = "sum" # a TOTAL
unit = "min"
plot = "column"
[tracking.practice.metrics.focus]
aggregate = "mean" # a RATING - a sum would grow with how often you log
| Key | Where | Purpose |
|---|---|---|
records | activity | Frontmatter key holding a sequence of flat records. Omit for plain scalars read straight off the entry. |
group_by | activity | Record field the series split on — a category, a subject, a person. One series per distinct value. |
at | record | Not a config key but the per-record field that orders a set, so last reads the reading you meant. HH:MM, HH:MM:SS, or a 12-hour time with a meridiem. Needs no quoting — the colon keeps it text — but a colon-less at: 1800 is a number and never reaches the parser. All-or-nothing: cdno lint reports a value it cannot use, or a set only partly stamped. |
type | metric | bool | int | float | string | date. Optional. |
aggregate | metric | sum | mean | last | max | min. Defaults to sum. |
group_by | metric | Overrides the activity’s. "none" collapses across records for an entry-level series. |
derived | metric | An expression computing this metric from sibling fields, e.g. "km * rate_per_km". Evaluated per record, before aggregation. Declare type on it and the vault refuses to open. |
unit | metric | Display unit (min, kg, EUR). Carried through to the chart and the MCP series. |
label | metric | Display name for the series, when the metric’s key is not what you want on a chart (resting_hr → Resting heart rate). |
plot | metric | none | line | column | area | scatter. Defaults to none. Chooses the mark the chart draws, and whether a chart consumer draws it at all — see the note below. |
Derived metrics
Some tracked quantities are products of others — a cost from a rate and a distance, a load from a weight and a count. Rather than making whoever writes the entry pre-compute them, declare the expression:
[tracking.commute.metrics.km]
aggregate = "sum"
[tracking.commute.metrics.rate_per_km]
aggregate = "last"
[tracking.commute.metrics.cost]
derived = "km * rate_per_km"
aggregate = "sum"
unit = "EUR"
It is evaluated per record, then aggregated — not derived from the aggregates. With two trips
of 10 km at 0.50 and 20 km at 0.25, cost is 10.00; deriving from the totals would give a
different, wrong number the moment the rate varies.
The grammar is deliberately tiny — one binary operation, nothing else:
expr := operand OP operand
operand := field-name | number
OP := '+' | '-' | '*'
- No
/. Division is the one operator that manufactures NaN and infinity, and those must never reach an aggregate. Multiply by the reciprocal, or pre-compute the ratio. - No parentheses, calls, chaining or recursion, and no deriving from another derived metric.
- Field names are letters, digits and
_, not starting with a digit. Hyphens are excluded becausea-bwould be indistinguishable from a subtraction. - Numbers are plain decimal — digits, an optional leading
-, an optional.. Exponent notation (1e-3) is not supported; write the value out in full. - Every operand must name a metric the same activity declares. A typo is a vault-open error
naming the field, rather than a silently empty chart. The cost of that requirement: an operand
that exists only to be multiplied — a rate, say — still becomes a metric of its own. Leave its
plotundeclared (the default) and it is not charted alongside the result — see below. typemust be omitted — the output is numeric by construction, so declaring one can only contradict it.- A record missing an operand contributes nothing — a gap, on the same rule as a plain metric. So does a result that is not finite.
Choosing the aggregate is the whole point, and it follows from what the number is:
| Kind | Examples | Aggregate |
|---|---|---|
| Total | amount spent, pages read, minutes practised | sum |
| Level | account balance, a measurement, a top set | last, max |
| Rate or rating | a score out of ten, perceived difficulty | mean |
Notes and limits:
- An absent
[tracking]section is not an error — an undeclared activity keeps being served from its body table, so nothing forces a migration. - Declaring is checked at vault-open, not at first chart render: an unknown
aggregate, a mistyped key, or a blankrecords/group_byfails when the vault is opened, naming the key. - An activity may declare no metrics at all. That is a complete use — recording that something happened, with nothing to aggregate.
windowis reserved for a future time-reduction axis (month-over-month deltas, rollups) and is a load error today, so adding the behaviour later is not a breaking change.- A declared activity’s body table is no longer read once its frontmatter yields a series, so
the same metric can never appear twice under two disagreeing numbers. This rule is unconditional
— it keys on the frontmatter derivation’s full produced set, not on what any individual metric’s
plotsays, so a declared-but-unplotted metric still suppresses its body-table equivalent. plotchooses the mark, and gates whether it is drawn at all. A declaredline/columnis used as the chart’s mark (anareaorscatterresolves to the closest of the two the chart draws).plot = "none"— the default for a declared metric that names no mark — is still emitted and still queryable over MCP, but left out of any chart pane. Declaring an activity is an explicit act, and is allowed to change what is drawn: its frontmatter series replace its body-table ones (the rule above, unaffected by this), and only the metrics that opt into a mark are charted.
Focus
The [focus] section sets how far back the focus is read.
All three keys are optional, and an unknown key in the section is a hard error, so a typo cannot
silently turn a window off.
[focus]
carry_over_days = 1
paused_lookback_days = 14
max_window_days = 366
| Key | Default | Meaning |
|---|---|---|
carry_over_days | 1 | The focus is the newest open start or resume among the daily notes from today - carry_over_days to today, unless a close follows it. 1 means a start left open yesterday is still the focus this morning and expires after that; 0 reads today only, as before the window existed. Calendar days, not rolling hours. |
paused_lookback_days | 14 | How far back a pause is still offered: by cdno action resume, by the “Last paused” line of cdno now, and as last_paused in the MCP orientation and project-context reads. 14 covers a weekend and most holidays; raise it if you are away longer. |
max_window_days | 366 | A ceiling on the two windows. A vault whose carry_over_days or paused_lookback_days exceeds it fails to open, with an error naming the key, its value and the ceiling, so a typo like 14000 cannot turn every read into thousands of file checks. Raise it if you really want a longer window. |
Both windows are read from the daily log each time; nothing is cached, so changing a value takes
effect on the next cdno command. A running MCP server keeps the config it started with, so
restart it (reconnect the client) for an agent to see the change. See cdno now and
Troubleshooting.
Projects
The [projects] section holds policy for project maps. Its one key, holder_sentinels, lists the
values of a map’s held_by: (the name of whoever else is moving the map) that name you or nobody.
Such a value reads as no holder, so the map stays yours and counts, exactly as a map with no
held_by: line does. The key is optional; leaving it out gives the default below, so a vault that
never sets it behaves as before. An unknown key in the section is a hard error, as in [focus], so
a typo cannot silently keep the default.
[projects]
holder_sentinels = [
"me", "I", "myself", "(mine)",
"someone", "somebody", "nobody", "unknown", "tbd", "n/a", "none", "-", "?",
]
| Key | Default | Meaning |
|---|---|---|
holder_sentinels | the list above | Values that name you (me, (mine), which is how the log writes “no holder”) or nobody (tbd, ?). Each value is trimmed and compared in any case, so " TBD " matches tbd. Setting the key replaces the whole list: add your own name or your own language’s words (holder_sentinels = ["yo", "nadie"]), or set [] to accept any name. |
The limit. cdno cannot know your own name. held_by: "<your name>" reads as someone else, so
the map stops counting, unless your name is on the list. If you want it refused by
cdno project hand-off and read as yours, add it (and keep
the default entries you still want, since setting the key replaces the list).
A blank held_by: names nobody under any list, so it is not in the default and you need not add
it. A held_by: that is not a plain name (an unquoted [[people/ana]], a number, a date, quoted
or not) also reads as no holder; quote a wikilink (held_by: "[[people/ana]]") for it to count as a
name.
Templates
Templates live in .cuaderno/templates/ and are pure variable substitution. cdno init writes two
starters (daily.md, and concept.md for the concept type it declares); other built-in types use
their built-in default until you add a file. cdno selects the
most specific template that exists: a custom variant (e.g. tracking-gym.md), then a custom type
(e.g. project.md), then the built-in variant default, then the built-in type default. Template
field order is the canonical order
cdno normalise enforces.
The per-type placeholders that resolve at creation, with a worked example, are in Customising templates and frontmatter. An unknown placeholder is left verbatim in the note.
Static [variables] resolve in any custom template (e.g. {{author}}). Prompted
[variables.prompt] are gathered at creation (via --var name=value, an interactive prompt, or a
static default) — see the
tutorial.
Editing from the CLI
cdno config edits this file without hand-editing it, and every write goes through the same gate:
cdno config show # verbatim, comments and key order intact
cdno config validate # the exact check running a vault performs
cdno config edit # $EDITOR round trip
cdno config note-type set --name people --folder people --required name,email
cdno config field set --note-type people --field email --type string
cdno config var set --name author --value "Your Name"
Saving is gated so an edit can never leave the vault unopenable:
- The whole candidate is validated first — TOML parse,
ignoreglobs, and the[note_types.*]/[schemas.*]rules. If it would not reopen, the save is refused and the file is left untouched. - A content-hash compare-and-swap then guards against a concurrent hand-edit: if the file changed on disk since it was read, the save is refused rather than overwriting the newer version.
- Only then is the candidate written verbatim, so comments and key order survive.
The structured setters merge: a flag you do not pass keeps its current value, so changing one key
cannot silently drop the rest of an entry. Pass an empty value to clear one (--template ''), or
the paired negative flag for a boolean (--no-append-only).
See the config reference for every subcommand.
See also
- Customising templates and frontmatter — the tutorial.
- Configuration — the conceptual overview.
- Frontmatter fields — what
extra_requiredextends. - Custom note types — the
[note_types.*]table in full.
Custom note types
Cuaderno ships twelve built-in note types (see Note types). If you need
an entity the built-ins don’t cover — people, books, clients, recipes — you can declare your own
custom note type under [note_types.<name>] in .cuaderno/config.toml. No recompile, no plugin.
What a custom type is (and is not)
A custom type is schema-only. It gives you:
- a folder its notes live in,
- enforced
required/optionalfrontmatter fields (checked bycdno lint), - an optional template,
- canonical frontmatter ordering (
cdno normalise), - full participation in indexing, full-text search, backlinks, and
cdno note; - a creation line in the daily log (
<type> created [[<folder>/<slug>]] — <title>); - in-place revision with a logged reason (
cdno note revise, orrevise_noteover MCP), unless the type is declaredappend_only = true.
It does not get bespoke behaviour. The 5-project cap, project state history, commitment
aggregation, tracking streams, and action lifecycle belong to specific built-in types and are not
available to custom types. A custom type is therefore invisible to cdno orient, the project cap,
and the commitments view — by design. If you need that behaviour, use (or extend) a built-in type.
Declaring a type
[note_types.person]
folder = "people" # required — vault-relative, must not be a built-in folder
required = ["name"] # fields that must be present and non-null (lint errors otherwise)
optional = ["role", "org"] # fields that may be present; part of the canonical order
template = "person.md" # optional; defaults to "<name>.md" under .cuaderno/templates/
append_only = false # optional; `true` makes `note revise` refuse the type (lint does not check it)
title_field = "name" # optional; which frontmatter field holds the display title (default: the H1)
date_field = "met_on" # optional; which field carries the note's date (for date-filtered search)
Validation runs at vault-open, so a malformed declaration fails fast. Cuaderno rejects a type whose:
folderis empty, has surrounding whitespace, escapes the vault (.., absolute,\), or collides with a built-in folder (projects,journal, …) or another custom type’s folder;templateis not a bare filename;title_field/date_fieldnames a field that isn’t inrequired/optional;- name shadows a built-in type (
project,daily, … — case-insensitive). Built-in names are reserved so a straytype:typo can’t silently mint a type.
Bundled types
Some custom types ship with cdno itself. They are ordinary declarations, not built-ins: cdno init
writes them into a new vault’s config, and you may edit or delete them like any type you declared.
Today there is one, concept.
A vault created before a bundled type existed does not have it. Install it with
cdno config note-type install --list # what ships, and whether this vault has it
cdno config note-type install --name concept
which writes the type’s template, folder and declaration, each only when absent, and never changes a declaration you already have. See Installing a bundled note type for the report and the refusals.
Creating notes
cdno note create person --title "Ada Lovelace" --field name=Ada --field role=advisor
cdno note list person
--field name=value is repeatable; each key must be a declared required/optional field, and
every required field must be supplied. --var name=value supplies a template’s
prompted variables.
The note is written to <folder>/<slug(title)>.md. Creation is logged to today’s daily note as
<type> created [[<folder>/<slug>]] — <title>, so do not log it again by hand.
The template is looked up in .cuaderno/templates/ under the name template gives, or
<name>.md when the declaration names none. There is no built-in fallback for a custom type: if
that file exists (.cuaderno/templates/person.md), it is rendered; otherwise Cuaderno
synthesises a minimal note
— a frontmatter block of your fields plus a # <title> heading — so a type works before you author
its template. (Field values are always emitted as strings, so a value with a colon, #, or newline
round-trips safely; author a template if you need richer frontmatter shapes for keys you do not pass
as fields.)
Whatever a template renders, the frontmatter that is written carries type: <type> and every field
you passed (including --origin) with the value you passed: a string equal to it, or a plain
number, boolean or null that reads back as the same text (so priority: {{priority}} with 5 stays
the number 5). If the rendered frontmatter already does, the note is written exactly as rendered. Otherwise Cuaderno repairs it — a missing
field is appended, an unquoted origin: {{origin}} that YAML would read as a list (or reject) is
rewritten as a quoted string — and re-serialises just the frontmatter block, which drops that
block’s comments and quoting style; the note body is left as rendered. So a template that forgets
{{origin}}, or renders no frontmatter at all, still records every field you supplied.
If the rendered frontmatter is not valid YAML at all (for example an unquoted origin: {{origin}}
given two links), it cannot be repaired key by key, so Cuaderno rebuilds it: type, then the
type’s declared fields in declared order, each with the value you passed or, for title, slug,
created and date, the value create computes. The note body is again left exactly as rendered.
Any key that only the template wrote is lost in that case. The simplest way to avoid it is to
leave origin out of the template and let the repair add it.
The body (--body-file, or body over MCP) is written without its title heading: the engine
writes the # <title> H1, and a leading # <title> line in the body is dropped. It fills the
template’s {{body}} placeholder, or is inserted after the H1 when there is none. The body wins
over any other value of {{body}}; without one, a declared body field, a [variables] value or
a prompted body fills the placeholder as usual, and it renders empty only when nothing does. The
body is substituted as raw text, so keep {{body}} in the note body: in the template’s frontmatter
a multi-line body can add or break YAML keys, and nothing refuses that.
From an MCP client, the equivalent tool is create_custom_note ({ type_name, title, fields, vars, body, origin }).
Discovering placeholders and searching
cdno templates vars personlists the{{placeholders}}apersontemplate may reference — its create-path built-ins (title,slug,created,date,body) plus your declared fields.cdno templates eject persondoes not apply — a custom type has no built-in template to materialise; author.cuaderno/templates/person.mdby hand.cdno search <query> --type personfilters results to that type.--typeaccepts any built-in or custom name; a name that is neither errors with the valid set. Shell completion offers your vault’s types.
Relationship to [schemas.*]
[note_types.<name>] defines a type: its folder, its required and optional fields, its
template. [schemas.<name>] attaches typed field declarations to a type, and it keys a custom
type as well as a built-in. A name under [note_types] may not be a built-in.
For a custom type, a [schemas.<name>.fields.<field>] declaration (see
Configuration reference) does this:
- Lint type-checks it. When the field is present in a note,
cdno lintwarns if its value does not match the declaredtype(orvalues), e.g.field `difficulty` is not a valid int for note type `concept`. A warning, never an error. A value passed with--field(orfields) at creation is always written as a string, so a typedint,floatorboolfield set that way draws this warning; set it afterwards withcdno frontmatter set, which writes the declared type. settable = truemakes it writable withcdno frontmatter setor theset_frontmatterMCP tool, type-checked and without a hand edit. The key must already be in the note’s frontmatter: the setter rewrites a field, it does not add one.list_note_typesreports it in the type’sfields, so an assistant can see it.
Two parts of a schema do not apply to a custom type:
extra_requiredis not enforced. A custom type’s required fields come from its ownrequiredlist, and that is whatcdno lintand creation check. Anextra_requiredname on a custom type only shows up inlist_note_types, as astringfield infields. Put the field inrequired.- A declared
defaultis not written at creation. A custom note’s frontmatter carries what the template renders plus the fields you pass; to give every new note a field, write it into the template.
The schema does not widen what --field accepts either: a key you pass at creation must still be
listed in the type’s required or optional.
Reading and revising
Any note of a custom type can be read whole with cdno open (<type>:<slug> works
as a reference) or the read_note MCP tool, and refined in place with
cdno note revise or revise_note: either the whole body
or one section at a time, with a required reason that is logged as
revised [[<path>]] — <reason>. Built-in types are refused (their sections belong to their own
commands), and so is a custom type declared append_only = true.
Worked examples
Tracking people walks a person type end to end — declaring it,
creating people, and linking them from your notes to answer “what was my last interaction with X?”.
The concept library is the bundled type cdno init declares for
you (cdno config note-type install --name concept adds it to an older vault), and Building a concept library walks its creation with
--body-file and --origin, and its revision.
Recurrence syntax
Periodic commitments on a stewardship (--every on
cdno stewardship add-periodic, or recurrence on the MCP
add_periodic_commitment tool) use a small canonical vocabulary.
| Value | Meaning |
|---|---|
daily | Every day |
weekly | Every week |
monthly | Every month |
every N months | Every N months (e.g. every 3 months for quarterly) |
yearly | Every year |
Quote any value containing a space on the command line.
Examples
cdno stewardship add-periodic --stewardship health --title "Dental check-up" --every "every 6 months" --next 2026-09-01
cdno stewardship add-periodic --stewardship finances --title "File quarterly taxes" --every "every 3 months" --next 2026-07-15
cdno stewardship add-periodic --stewardship health --title "Annual physical" --every yearly --next 2026-11-01
Each line becomes a row in the dashboard’s ## Periodic Commitments section and feeds the aggregated
cdno commitments view, advancing to its next occurrence as dates pass.
See also
Troubleshooting
Common situations and how to resolve them.
“not inside a Cuaderno vault”
A command can’t find a vault. Cuaderno discovers one by walking up from the current directory for a
.cuaderno/ folder, then falls back to CUADERNO_VAULT_PATH. Fix one of:
cd ~/notebook # run from inside the vault, or
cdno --vault ~/notebook <command> # point at it explicitly, or
export CUADERNO_VAULT_PATH=~/notebook
See Initialise a vault.
Search or links look out of date
The index is a cache; it’s reconciled automatically each run, but a large external edit or a sync conflict can occasionally confuse it. Rebuild it:
cdno reindex
The Markdown files are always authoritative, so a rebuild is safe. See Business rules.
lint is failing
Run it to see the specifics:
cdno lint # errors fail; warnings are listed but non-fatal
cdno lint --strict # warnings fail too
Errors are usually an unknown type: or invalid frontmatter; warnings are typically broken
wikilinks. Fix the reported file, or run cdno normalise if the issue is field
ordering. See Frontmatter fields.
cdno now says nothing is started, but I started something
Three causes, and cdno lint tells the first apart from the rest.
If you wrote the log line by hand, it is almost certainly not in the shape the readers accept. It has to be exactly:
- **09:30**: started [[surrogate-model]] — Draft the methods section (deep)
The - bullet and the **HH:MM**: stamp are what make it a log entry at all, and the separator
must be a real em dash (U+2014), not a hyphen. A near-miss is skipped in silence by design — prose
beginning “started something” must never register as a focus — so cdno lint reports it instead,
naming the line and the likely cause. Run it and fix what it points at. The same goes for paused,
resumed and action promoted on lines.
Otherwise, check the date. The focus is read from today’s daily note and the
[focus] carry_over_days days before it (default 1, so yesterday’s too); a start logged earlier than
that and never closed has expired. Pick the work up with
cdno action resume (it also finds a recent pause), or start it
again.
The third cause surprises people who remember the older behaviour: focus is one slot. If you
started X, then started Y, and finished Y, nothing is in focus. X was displaced the moment Y began,
and closing Y does not bring it back, because nobody chose to go back to it. Start X again, or, if
you paused it along the way, cdno action resume. Logs written before the slot rule read the same
way, so a day that used to report X as the focus now reports none.
cdno now says I’m still on yesterday’s thing
The focus survives midnight on purpose: a start left open yesterday is still the focus this morning,
with the day named (since Saturday 14:05). That is the one-day window,
carry_over_days. You have three ways out:
- It is still what you are doing:
cdno action resumere-anchors it to today (picked up 08:50 today (started Saturday 14:05)) and keeps it from expiring tomorrow. - You stopped but will come back:
cdno action pause --next "…", andresumebrings the hint back. - You do not want anything to carry overnight: set
carry_over_days = 0in.cuaderno/config.tomland the focus is read from today’s note only. A running MCP server keeps the config it started with, so restart it (reconnect the client) for an agent to see the change.
Finishing it with cdno action complete or drop clears it as ever.
action start refuses with focus_open
Something is already in focus, and starting a second thing would leave the log silent about the move. The message names what is open and the command to run:
Error: Draft the methods section is already in focus on surrogate-model (since 09:30).
To move on to this instead: cdno action switch --project other-project --query CI
Or complete or pause it first.
cdno action switch pauses what is open and starts the new action in one step; cdno action pause
then start does the same in two. If the open focus is the action you are trying to start, start
says so: nothing to do when it was started today, and cdno action resume when it was carried over
from an earlier day.
This is most often noticed the first morning after upgrading. A start left open yesterday and never
closed is now carried into today, so the first start of the day is refused where it used to stack
silently. Resume it, pause it, complete it, or switch. If you would rather keep the old behaviour,
set carry_over_days = 0 (and restart a running MCP server, which keeps the config it started with).
Over MCP the refusal is a focus_open rejection with a remedy; see
Write tools.
A prompt appears when I wanted automation (or vice versa)
Write commands prompt for missing required flags only in an interactive terminal. In scripts, pipes, or CI they error instead. Force non-interactive behaviour explicitly:
cdno project create --title "X" --context work --no-interactive
Conversely, if a command errors about a missing flag when you expected a prompt, your stdout probably isn’t a TTY (it’s piped or redirected). See CLI overview.
Can’t create a sixth project
That’s the five-project cap: five projects count, that is, are active and name no one else as their holder. Free a slot first: park an active project, or close one that has finished or is not going to happen.
cdno project park --slug some-active-project # or: project complete / project drop
cdno project activate --slug the-one-you-want
(New projects created while at the cap are created parked rather than rejected.)
My handed-off project still counts
A project you handed off is counted again when its held_by: does not read as a name. Run
cdno project show <slug>: it ends with Counts toward the cap: yes or no, and prints
Held by: <name> only when the holder is honoured. cdno lint names the cause. The three usual
ones:
- A sentinel. The value names you or nobody:
me,myself,(mine),someone,tbd,?, … A map with such a value is yours, so it counts, and lint warns. Name who really has it withcdno project hand-off --slug <slug> --to "<name>". The list is the vault’s[projects] holder_sentinels; if the holder’s real name is on it (added when you replaced the default list), take it off. - A placeholder. A blank
held_by:names nobody under any list, whatever the config says. Fill it in, or delete the line. - An unquoted wikilink.
held_by: [[people/ana]]is a YAML list, not a name, so the holder is not honoured and lint reports an error. Quote it:held_by: "[[people/ana]]". The same goes for a number or a date.
If you cannot name who has the project, it is yours, and counting it is right. Handing it off is not a way to make room at the cap: park, complete or drop a project instead.
The reverse also happens: held_by: "<your own name>" does not count, because cdno cannot know
your name. Take the project back (cdno project take-back --slug <slug>), and add your name to
holder_sentinels if you want it refused in future.
Writes are refused with a method version error
The vault records the version of the Research Logbook Method it follows ([vault] method_version,
see configuration), and every write checks it under the write
lock. Reads keep working; only writes are refused, when the major version differs:
- “…an older major version than the RLM … this cdno implements”: the vault needs migrating.
Run
cdno migrate --dry-runto see what changes, thencdno migrate. Migration is CLI-only, so an MCP client hitting this error needs you to run it in a terminal. - “…a newer major version than the RLM … this cdno implements — upgrade cdno to write to it”: another machine, or a newer cdno, migrated the vault. Upgrade this cdno (and restart any MCP server running the old binary).
A minor difference never refuses a write; it prints a one-line notice on stderr instead. Do not
edit method_version by hand to silence either error: the stamp goes with the migration’s log
entry and any file steps.
A link to an old name still works in cdno but not in my editor
After cdno note rename every link in the vault is rewritten to
the new name, and the old name is kept on the note as an alias so cdno keeps resolving it. A link
that was written with the old name afterwards (typed from memory, or brought back by a sync merge
from a machine that had not seen the rename) still works in cdno, but an editor such as Obsidian
or Neovim follows the file name, not the alias, and cannot open it.
cdno lint finds them: [[old]] names a renamed note — now `new` . Leaving them is harmless,
since they still work in cdno. To rewrite every one to the current name in one go, run
cdno note rename new --relink (--dry-run first
to see them; over MCP, rename_note with relink: true): nothing moves, the old name stays listed,
and a relinked line is logged. This works for an archived action note too, which cannot be renamed.
A union merge brings back the whole hunk, so the lines it duplicated stay after the relink, as
identical text twice: the relink corrects addresses, it does not tidy the merge. Changing one link
by hand is fine too, even in a past log entry: it is the same address correction.
Rename refused: behind upstream
Error: the vault is 2 commits behind its upstream; sync the vault first (pull the upstream's commits, e.g. with `git pull`), or pass `--skip-sync-check` to rename anyway
cdno note rename will not rewrite many notes in a vault whose other clone has already changed them.
Sync the vault (git pull, or your sync tool) and run the rename again. If you are sure nothing
unpushed on the other side touches these notes, pass --skip-sync-check (over MCP, skip_sync_check
is for the person to decide, not the agent). cdno note rename --dry-run --skip-sync-check shows the
plan of a vault that is behind without writing.
If the message is instead a git operation is in progress in the vault (…); finish or abort it, then rename (code git_operation_in_progress with --json; text mode prints no code), a merge, rebase
or cherry-pick was left paused in the vault. Finish or abort it (git status says which); --skip-sync-check does not
override this one.
--json output won’t parse
--json is only honoured by read verbs and the write verbs that emit a result; maintenance and
interactive commands (init, lint, reindex, normalise, triage, review, weekly) ignore
it. Under --json, write verbs run non-interactively, so there are no prompts mixed into the output.
See JSON output.
Claude doesn’t see the tools
For the MCP server (cdno-mcp):
- Make sure
cdno-mcpis on the client’sPATH, or use an absolute path in the configcommand. - Set
CUADERNO_VAULT_PATH(or rely on working-directory discovery). - Restart the client after editing its MCP config.
See Connect to Claude.