seamless docs

Notes, projects & capture

Work artifacts, project scope, and SSRF-safe URL capture - the nine tools around the edges of memory.

On this page

A note is not a memory

This is the most common confusion in the whole system, so it is worth being blunt about it.

Memory Note
Answers "What is true about this project?" "What did we produce?"
Length One idea, one line of description However long the artifact is
Lifecycle Superseded and archived Written, occasionally updated
Reaches a briefing Yes - this is what agents get injected No; found via recall
Good examples A constraint, a gotcha, a decision Research findings, a meeting summary, a design record

The test: would a future agent need this injected into its context before it starts working? If yes, it is a memory, and it needs to fit in a description. If it is something you'd want to find and read in full when the topic comes up, it is a note.

Writing a journal entry into memory is the classic failure: it is too long to inject, too specific to generalize, and it pushes real constraints out of the briefing's budget.

Agent-created notes are automatically tagged created-by:agent.

Four ways to change a note

Notes are long, and the transport caps a request body at 1 MB, so resending a whole note to fix one paragraph is both wasteful and the thing most likely to fail on the biggest artifacts.

You want to Use What happens
Fix or restructure part of the body notes_edit Exact search/replace, all-or-nothing, returns a diff
Replace the body, or change title/description/project/tags notes_update Field-wise patch; omitted fields are untouched
Add to the end notes_append A UTC-timestamped line joins the body
Remove an artifact that should not exist notes_delete Gone, with no pointer left behind

notes_edit takes the note's id and a list of {old_string, new_string} edits. Each old_string must match the current body exactly and uniquely - or pass replace_all - and if any edit fails to match, nothing is written. That refusal is the feature: a partial apply, or a fuzzy match landing somewhere plausible, is the silent corruption the exact-match contract exists to prevent.

notes_update gains the same staleness guard (expect_hash) plus tags_add and tags_remove, which are race-friendlier than replacing the whole tag list and are the only way to clear a tag - an empty tags array reads as absent. See Concurrency.

Every note mutation now records a note.written event, and notes_read records note.read - the note-side twin of memory.read, which is what lets note demand count toward recall's utility nudge.

Notes are how plans get their narrative

A plan is not a primitive - it is a composition keyed by plan:<slug>. Tag a note plan:<slug> and it joins that plan's supporting context, so the next agent inherits the design and the reasoning behind it, not just the step list. See Tasks for the step half of the composition.

Projects and scope

project_list and project_create manage the scopes everything else inherits. Most agents never call either: a git repo maps itself on the first session and resolves its project from the agent's working directory, and session_start binds it.

The global project slug is the deliberate cross-project scope. It is a token you pass on purpose, never a default you fall into - see the fail-closed rule in the MCP API overview.

capture_url is SSRF-safe on purpose

capture_url fetches a URL and returns its content as markdown. It is the one tool that makes an outbound request on an agent's behalf, so it is guarded: destination ports are restricted to capture.allowed_ports (80 and 443 by default, never "any port"), and the fetcher refuses to be talked into reaching things it should not. See Configuration.

Scope resolves before the network fetch, so an ambiguous durable destination fails without making a request. Success returns the note's id, slug, title, resolved project, and source_url - the URL as requested; redirects are followed but do not rewrite it. The fetcher validates the initial URL and every redirect, rejecting non-HTTP schemes, private/loopback destinations, and disallowed ports. Size is bounded by truncation rather than rejection: at most 2 MB of the response is read, and the stored readable body is capped again at 50,000 runes with a visible [content truncated] marker.

notes_create

Create a work note -- a research finding, decision record, meeting summary, or any artifact long enough to deserve its own file. Auto-tagged created-by:agent. Notes carry the long form; memory_write carries the compact durable knowledge a future session must not miss, so put the write-up here and the one-line lesson there rather than duplicating either. Pass plan=<slug> when the note is a plan's narrative or supporting context, so it joins that plan's composition beside its tasks. Do not use this for what the repo, AGENTS.md/CLAUDE.md, or the current conversation already records, and use notes_append to extend an existing note rather than creating a near-duplicate of it.

Parameter Type Required Description
title string yes note title
body string yes markdown body (aliases: content, text)
description string no optional one-line summary
plan string no optional plan slug (plan:<slug> convention): tags this note into that plan's composition so it surfaces on the Plans screen alongside its tasks_add plan=<slug> steps. Use it whenever this note is a plan's narrative or supporting context.
project string no project slug; defaults to the bound/ambient session's project. An unknown slug CREATES that project -- naming a new one is normal and never an error. Pass project=global ONLY for knowledge that belongs in EVERY project's briefing; it is not a neutral default. With no session and no explicit project the call is rejected as ambiguous. A session bound to a confidential or sealed project can write ONLY into that project.
source_url string no optional source URL
tags array no tags (a comma-separated string is also accepted)

notes_read

Read a note by id, or by slug within the current project (falling back to a global note of the same slug).

Parameter Type Required Description
id string no note id (ULID); pass exactly one of id or slug
project string no project slug for the slug lookup; defaults to the bound session's project
slug string no note slug, as briefings, plan compositions, and notes_create responses name notes (alias: name)

notes_update

Update a note's fields by id (title, description, body, project, tags). Omitted fields are untouched; the slug and id stay stable. body replaces the WHOLE body, so pass expect_hash (notes_read returns it) to have the write refused rather than silently overwriting an edit that landed after you read the note -- and use notes_append when you only mean to add to it. Tags come in three flavors: tags replaces the whole set, while tags_add and tags_remove edit it in place; prefer add/remove, since a replace discards whatever another agent tagged in between and tags_remove is the only way to clear a tag at all.

Parameter Type Required Description
id string yes note id (ULID)
body string no new body, replacing the whole body (aliases: content, text)
description string no new description
expect_hash string no optional precondition: the content_hash you last read for this item (memory_read/notes_read return it). The write is refused if the stored file has changed since -- another agent or the owner edited it -- so re-read and re-apply your change instead of overwriting theirs. Omit it to write unconditionally.
project string no new project slug ("" or "global" = global scope)
tags array no tags, replacing all (a comma-separated string is also accepted); an empty list is read as absent and leaves the tags untouched -- to drop a tag use tags_remove
tags_add array no tags to add, leaving the rest in place; a tag already on the note is not duplicated. Applied after tags.
tags_remove array no tags to drop, matched exactly; a tag the note does not carry is ignored. Applied last, so it also removes what tags/tags_add just set.
title string no new title

notes_edit

Edit an existing note in place with exact search/replace, by id, instead of resending its whole body through notes_update. Each edit's old_string must match the current body exactly and uniquely (or pass replace_all); all edits apply together or none do, so a failed match changes nothing. This is the tool for correcting or restructuring part of a long note -- notes_append only ever adds, and notes_update replaces the whole body. Returns a unified diff of what landed plus the new content_hash.

Parameter Type Required Description
id string yes note id (ULID), as notes_create returns and briefings and plan compositions carry
edits array yes ordered list of exact search/replace edits, applied in order to the CURRENT body. Each is {old_string, new_string, replace_all?}. old_string must match the body EXACTLY (whitespace and indentation included) and must be unique unless replace_all is true; include surrounding lines to make it unique. All-or-nothing: if any edit fails to match, nothing is written.
expect_hash string no optional precondition: the content_hash you last read for this item (memory_read/notes_read return it). The write is refused if the stored file has changed since -- another agent or the owner edited it -- so re-read and re-apply your change instead of overwriting theirs. Omit it to write unconditionally.

notes_append

Append a UTC-timestamped line to an existing note's body, by id. Use it when a note is already the right home for what you learned -- a running investigation log, a decision record gaining one more data point -- so the note keeps its id, slug, and place in any plan composition instead of fragmenting into near-duplicates. Appending only ever adds: use notes_update to correct or restructure what is already there, and notes_create when the finding deserves an artifact of its own. Needs the note's id (ULID), which notes_create returns and briefings and plan compositions carry; notes_read resolves one from a slug.

Parameter Type Required Description
id string yes note id (ULID)
body string yes text to append (aliases: content, text)

notes_delete

Delete a note by id: the markdown file leaves the disk and its index row goes with it. Permanent, and it leaves no pointer behind, so reserve it for notes that should never have existed -- a duplicate, a write into the wrong project, an agent's own scratch. To fix a note's content use notes_update, and to add to it notes_append; neither loses the artifact. A note tagged into a plan (plan:<slug>) is that plan's narrative for whoever inherits it, so read it with notes_read before deciding it is disposable.

Parameter Type Required Description
id string yes note id (ULID)

project_list

List every project (slug, name, description, isolation). Use it to learn the exact slug before a deliberate cross-project write or a project_create -- coining a near-duplicate of a slug that already exists is the failure this prevents -- and to see whether work already has a home. You usually do NOT need it to pick a scope: memory, note, and task calls inherit the project from the session binding, and passing project= is for writing outside that on purpose. Each row carries its isolation state (open|confidential|sealed), which is what a cross-project call has to respect: a confidential or sealed project is readable only from a session bound to it, and a sealed one takes no writes from outside either. It returns identity, not contents; to search what is inside a project, use recall.

Takes no parameters.

project_create

Register a project up front, with a human-readable name and an optional description. You rarely need this: any durable write naming an unknown project slug (memory_write, notes_create, tasks_add, capture_url, trial_record) already registers that project, and a git repo maps itself to one on its first session. Reach for this only to give a project a proper name and description BEFORE anything is written into it, or to create one you will not write to yet -- an auto-registered project is named after its own slug until someone fixes it. Call project_list first: coining a near-duplicate of an existing slug is the failure mode here. To divide an existing project into children, use gardener_split rather than creating them by hand. The slug defaults to a slugified name; "global" and "all" are reserved, and an existing slug is an error, not an update -- this never renames or edits a project.

Parameter Type Required Description
name string yes human-readable project name
description string no optional one-line description
isolation string no optional isolation state (open|confidential|sealed); omit for open. open shares normally. confidential means nothing leaves: agents in other projects never read this one, and agents bound to it cannot write outside it. sealed adds the inbound half -- agents here see only this project: no global memories, no family, no cross-project reads. Set it at creation for work that is sensitive from the start; there is no tool to change it afterwards, because tightening detaches family and parent links and is an owner decision, made on an owner surface. Isolation requires a standalone project, so do not create an isolated child of another project. One of: open, confidential, sealed.
slug string no optional explicit slug

capture_url

Fetch a web page (SSRF-guarded: private/loopback addresses are rejected) and save its readable content as a note. Returns the new note's id.

Parameter Type Required Description
url string yes http(s) URL to capture
project string no project slug; defaults to the bound/ambient session's project. An unknown slug CREATES that project -- naming a new one is normal and never an error. Pass project=global ONLY for knowledge that belongs in EVERY project's briefing; it is not a neutral default. With no session and no explicit project the call is rejected as ambiguous. A session bound to a confidential or sealed project can write ONLY into that project.