tnotes for AI agents
Use tnotes as a notes backend for AI agents: a headless CLI with JSON output, stable note ids, links and backlinks, no daemon and no API. Recipes for Claude Code, Codex, cron and shell scripts.
Why a CLI and not an MCP server
Every coding agent can already run shell commands. A CLI that prints JSON is the smallest possible integration surface: nothing to install on the agent side, nothing to keep running, nothing to authenticate. The vault is a folder of Markdown files, so an agent can also just cat and grep it — tnotes adds the index: titles, tags, links, backlinks, timestamps and fuzzy search.
The five commands an agent needs
$ tnotes ls --json # every note: id, path, title, folder, tags, links, backlinks, created, modified, preview$ tnotes search "standup" --json | jq '.[0].path' # best match first$ tnotes cat vault/weekly-plan # full text (--json adds "text")$ echo "- [ ] follow up with Ann" | tnotes new "Meeting notes" --tag work --stdin$ tnotes append meeting-notes "- [ ] send the slides"A note is addressed by its id (root/sub/stem, as printed by ls), a path, or a unique file stem. The id is the same in every command. --dir pins the vault for one invocation, so an agent never depends on the user's config.
Walking the note graph
links lists the [[titles]] a note references; backlinks lists the notes that reference it. A "what's related to X" prompt is one ls --json plus a jq filter:
$ tnotes ls --json | jq '.[] | select(.links[]? == "Weekly plan" or .backlinks[]? == "Weekly plan") | .title'"tnotes roadmap""Meeting notes"What the agent cannot break
- A note that is open with unsaved edits in the TUI is never overwritten: the TUI keeps its text and writes a conflict copy next to the agent's version, and
writereturns an error. - A note open and clean in the TUI follows
write/appendlive. newrefuses a title that already exists unless--duplicateis passed, and rejects a blank title.writerefuses empty input. A changed title renames the file and rewrites every[[link]]to it.trashmoves to.Trash/;restorebrings it back. Nothing is deleted outright.- Saves are atomic; a syncer or a second process never sees a half-written note.
Claude Code / Codex: a snippet for AGENTS.md
Drop this into the project's AGENTS.md or CLAUDE.md (adjust the path):
## NotesMy notes are a tnotes vault at ~/Documents/notes. Use the CLI, never edit files directly:- list: tnotes ls --json [--tag t] [--folder f] [--limit n]- search: tnotes search "<query>" --json- read: tnotes cat <id>- create: tnotes new "<Title>" [--tag t] [--folder f] --stdin (body on stdin)- append: tnotes append <id> "<line>" or --stdin- rewrite: tnotes write <id> --stdinIds look like vault/sub/stem and come from ls/search. Link notes with [[Title]]; tag with #tag in the text.Daily note from cron
# crontab: create today's note at 08:00 if it does not exist0 8 * * * tnotes new "$(date +%F)" --tag daily --folder journal --stdin <<< "- [ ] " 2>/dev/null || trueHandy jq one-liners
$ tnotes ls --json | jq -r '.[] | select(.tags | index("work")) | .id' # ids tagged #work$ tnotes ls --json | jq -r 'sort_by(.modified) | reverse | .[0:5][] | .title' # five most recent titles$ tnotes ls --json | jq -r '.[] | select(.preview | test("\\[ \\]")) | .title' # notes with open tasks$ tnotes ls --json | jq '[.[] | .tags[]] | group_by(.) | map({(.[0]): length}) | add' # tag countsSee the CLI reference for every flag, and sync and encryption for running agents against a synced vault.