Skip to content

Context layer

Guides are the half of Mossbear your agent is graded against. The context layer is the other half: the notes, references and other files you want an agent to have, held in one account and delivered into whichever repo you are standing in.

A bundle groups context by project or topic. It has an id and a url-safe slug, and the slug is what pull_bundle, create_artifact, file_artifact and mossbear pull <bundle> all address.

An artifact in a bundle is one of three types: guide, content, or asset. Two of those are rows in one table; a guide is a row in a different one, which is why it behaves differently everywhere below.

Membership is the only thing that ties an artifact to a bundle, and one artifact can belong to several: an update through update_artifact propagates to every bundle the artifact is in.

MCP create_artifact and update_artifact return artifact.url after creating or updating an artifact. pull_bundle, list_inbox, and pull_inbox return url on each artifact. When using writeToDir, the reply pairs successful paths with review URLs in links: each entry carries itemId, path, and url. Skipped files have no written-path link. An agent can save the URL from a pull and include it in chat after editing the local file; the URL opens the canonical artifact, so it reflects edits once they sync.

The import tool returns guideId and url in each per-file result when the server supplies a canonical guide identity. Older servers may omit that identity; the agent should not guess a link from a file path. A conflict result links the existing guide for inspection and still reports that the edit was refused.

URLs use the configured dashboard address, including a local or staging dashboard. Artifact URLs open their authenticated pages; inbox URLs open the note in Library’s Inbox view. Signed-out readers are directed through login. These links do not change sharing permissions.

From the dashboard, a person can leave a comment on a bundle artifact’s page — a request about the artifact, never a change to it. (A guide picked up before it had a body of its own is the one exception with nowhere to write one; every guide authored since does.)

Comments are announced, and read on demand. Before your first turn, mossbear refresh says how many are waiting and names the command that reads them; the bodies come from mossbear comments, the list_comments tool, or pull_bundle, which returns a comments field on the artifact a comment is about. Each body arrives wrapped in the same untrusted markers as the rest of what a person wrote, because it is a request from someone in your workspace, not an instruction from Mossbear.

Findings Mossbear raised are listed too, marked as drafts. When Mossbear raises a finding about one of your guides (what goes wrong, with the evidence behind it, and no replacement text), list_comments and mossbear comments list it to your own agent before you have looked at it, and name it as a draft. Your agent shows it to you and changes nothing until you say so. Press Accept on the finding in the dashboard and it becomes a request like any other comment: it is announced at session start and rides pull_bundle. Your agent then decides what to change. Teammates never see a finding raised from your sessions.

The announcement is capped; the read never is. A comment that is still open stops being announced in a repo once you have been told about it a few times there, so silence before your first turn means “nothing new to interrupt you about” and never “there are no comments”. mossbear comments returns every open comment however often it is asked. The cap is kept per reader and per repo, which is what keeps one person’s silence from becoming everyone’s, and why it survives a fresh clone.

There is no reply tool and no thread. An agent answers a comment the way it answers anything else about an artifact: by editing it through update_artifact. A comment stays open until a person marks it done from the artifact’s page in the dashboard — reading it never counts as that.

The inbox is a query, not a container: a captured note that you have not archived and have not filed into any bundle, newest first. Filing it into a bundle is what takes it out. No bundle is special here, so a bundle named inbox has nothing to do with it.

That distinction has a consequence worth knowing before you write an allowlist. See --bundles below.

mossbear pull <bundle> does two things. It writes the bundle’s artifacts into .mossbear/context/<bundle>/, and it records a subscription in the repo’s .mossbear/config.json. The subscription is what makes the pull durable: mossbear refresh reads it on every run and keeps those bundles fresh, reconciling deletions so a note you delete in the dashboard stops reaching agents through this repo.

Run it with no bundle in an interactive terminal and you get a picker. With no terminal and no bundle it refuses rather than guessing.

Commit .mossbear/config.json, .mossbear/.gitignore, .mossbear/.gitattributes, .mossbear/evals/ and .mossbear/placement.jsonl, whichever of them exist. The placement file records where each delivered file lives, so a teammate’s clone knows which files a bundle manages. .mossbear/context/ stays out of git, and so does the placement file inside it, which records where that folder’s artifacts landed. A refresh that delivers a new artifact into .mossbear/context/ leaves git status clean.

If your repo has a .mossbear/docs-placement.jsonl from an earlier CLI, the next sync renames it to placement.jsonl and moves any .mossbear/context/ lines into the context folder’s own file. Commit that change once, and upgrade everyone who works in the repo: an older CLI doesn’t know the new name.

mossbear pull <bundle> records the subscription in this repo’s committed .mossbear/config.json, so everyone who clones the repo receives the bundle. --account records it against your account instead: nothing is committed, and every checkout you sync receives the bundle, on this machine and the next one. A bundle can be named by both, and that is one subscription in each place rather than a conflict.

By default each artifact lands where its own scope says: a project guide in the checkout, a guide you scoped to your home directory in your machine-wide guidance. --deliver overrides that for everything the subscription carries.

  • --deliver repo lands the bundle’s artifacts at their paths in this checkout.
  • --deliver global lands them in your machine-global skills, so they reach every repo you work in without appearing in any of them.

That is what lets a repo name a bundle to load into your global skills, and an organisation distribute one bundle of standard skills that each member receives wherever they work.

Omit --deliver and the subscription keeps whatever target it already had; a bundle that has never named one routes each artifact on its own scope, which is what an existing subscription — including your own my-guides — already does.

The repo’s target is the deliver key beside the bundle id in .mossbear/config.json, and your account’s is on your own subscription. They are independent: if the repo asks for repo and your account asks for global for the same bundle, you get both. Delivery into the checkout still follows the ordinary rule that git is the source of truth for everything outside .mossbear/ — an unattended mossbear refresh reports those files rather than writing them.

mossbear pull <bundle> --home claims this repo as the bundle’s home. From then on the bundle’s artifacts are published from this repo’s published branch (main unless you name another) and nowhere else: a change is a pull request here, reviewed here, and publishes once it merges. Everywhere else — a teammate’s mossbear complete push-back, an edit in the dashboard — the artifacts are read-only, and the attempt is refused by name rather than quietly dropped. Your local copy is never overwritten and never deleted; it is left exactly as it is and named in the report.

Delivery is untouched. Everyone who subscribes still receives the bundle on every sync; what a home takes away is the right to publish back.

That is what an organisation needs to distribute a set of standard skills: one repository is their source of truth, review happens there, and every member receives them wherever they work.

mossbear pull <bundle> --home again from a different repo moves the claim, and any member of the bundle’s workspace can move it — the claim is a property of the bundle, so a mistyped id is never unrecoverable.

Publication is how a merged change reaches everyone subscribed to the bundle this repo is the home of:

edit tracked skill or guide → commit/review → merge to main → subscribers refresh

It publishes only what landed on the published branch — main unless the block names another with published.branch. Bytes come from that branch, never from your working copy or the branch you have checked out, so what a subscriber receives is something a reviewer approved in this repository. Work on a feature branch is not published.

What this repo publishes is declared in .mossbear/config.json, beside the subscription and the project id:

{
"published": {
"paths": ["skills/**", "docs/guides/*.md"]
}
}

The declaration that counts is the one on the published branch, so a change to it publishes once it is merged there too.

mossbear publish fetches the published branch and publishes the declared set from it, with a preview first. --no-fetch publishes what your clone already has. mossbear publish <path> adds a path to published.paths; it publishes once that change is merged.

A repo can be the home of more than one bundle — plans in one, backlogs in another. Claim each with mossbear pull <bundle> --home, then route paths to a bundle by its slug under published.bundles:

{
"published": {
"paths": ["docs/plans/*-plan.md"],
"bundles": { "backlogs": ["docs/plans/*-backlog.md"] }
}
}

published.paths publishes to the one home bundle no entry under published.bundles names; if several are unnamed, publishing stops and says which to name. A path goes to every bundle whose declaration matches it, so a file named in two declarations is published to both and kept current in each.

Narrowing a declaration withdraws what it stopped covering. When a path you narrowed or removed is no longer declared for a bundle, publication withdraws that bundle’s copy, and the file stays in your repo. A change to .mossbear/config.json merged on the published branch withdraws what it stopped declaring. Any other undeclared copy waits for mossbear publish, which lists it as “no longer declared” before asking.

To empty a routed bundle, set its entry to [] ("backlogs": []); don’t delete it. Deleting an entry stops publishing to that bundle but withdraws nothing, because without the entry Mossbear cannot tell which bundle published.paths still covers. Emptying or deleting published.paths withdraws nothing for the same reason; to empty that bundle, name it under published.bundles with []. Only copies this repo published are withdrawn. A copy that is also filed in another bundle is kept and named, since withdrawing it would take it from that bundle too. A document someone created in the dashboard is never touched.

Delete the file and merge the deletion; the next publication withdraws the artifact. Subscribers stop receiving it, and their copy goes on the next refresh — unless they have edited it, in which case the file stays and Mossbear says it is no longer tracking it. Leaving the file in place would let guidance you deliberately deleted keep reaching agents in every repo that subscribes.

The artifact itself is not deleted. It stays in your dashboard, marked removed, with its history and the commit that removed it — so “where did this skill go?” has an answer. Merge a file at that path again and the next publication brings it back as a new revision of the same artifact.

Renaming a file is a deletion plus a publication, and Mossbear does not try to spot the pair: subscribers see the old path withdrawn and the new one arrive.

Every agent session hook checks whether the published branch has moved since that clone last published, and publishes what it changed. It reads only what your clone already has — the hook never fetches, so it never uses your git credentials and never deepens a clone. In practice that means:

  • After git pull brings in a merge, the next hook publishes it.
  • After git push from your clone, the next hook publishes it: pushing moves your clone’s own copy of the branch.
  • A fresh clone, such as a cloud agent session, publishes straight away.
  • A merge on GitHub that no clone has pulled publishes nothing until one does. Run mossbear publish to publish it on demand.

The branch is read from your remote — origin/main, not your local main — so an unpushed commit is never published: a later git pull --rebase could still rewrite it. A fork that publishes its upstream’s branch names that remote with published.remote ("upstream"), and a repo that publishes another branch names it with published.branch. Both are read from main, never from the branch you are on, so a branch cannot point publication at itself; the branch they name must declare the same two values. A clone that lacks the named remote publishes nothing. In a repository with no remotes, the local branch publishes directly.

A publication made this way has no preview and asks no question. The consent for it is the published block itself — opt-in, scoped to globs rather than to the repository, reviewable in a diff, and merged before any of it runs. mossbear publish typed by hand keeps its preview.

On a team, whichever member’s clone notices a merge first publishes it, under their own token, and each published revision records the commit it came from. Publishing from the merged branch is a convention the CLI follows, not a check the dashboard makes: any member of the bundle’s workspace can still publish to it.

If the published branch is rewritten (a force-push) after a revision was published from it, Mossbear does not publish over that revision; it says so, and a person decides which history is right. A session hook that finds a copy published from a commit its clone does not have says so too, since it cannot tell a newer commit from a discarded one without fetching; mossbear publish fetches the branch and settles it.

An account subscription can mute individual guides. A muted guide stops being delivered to you — in the checkout and in your machine-wide skills — while everyone else on the same bundle keeps receiving it. It is your own switch, not the shared one: a repo subscription lives in a committed file that everyone who clones the repo reads, so it has no mute at all.

A muted skill leaves your machine-wide skills one repository at a time, as each of your checkouts syncs. Mossbear only removes a file once no repository still asks for it, and a sync in one repository speaks only for that one.

--once exports the bundle into its folder and records no subscription. Nothing keeps it fresh afterwards. --out <dir> writes to a directory you choose and implies --once, because sync only refreshes the default location and a subscription pointing somewhere else would be a promise the sync does not keep. --out needs an explicit bundle: it targets one directory, so it is refused with the multi-select picker rather than quietly ignored.

--types, --query and --limit narrow an export. They apply to exports only, so they need --once or --out.

Terminal window
mossbear mcp

This runs an MCP server over stdio that exposes your bundles and inbox to any agent that speaks the protocol. It needs a token. Point your agent’s MCP client at it the way you would any other stdio server. Human-facing messages go to stderr, because stdout is the JSON-RPC channel.

The tools it registers:

ToolWhat it does
list_bundlesList the bundles this server can reach, with id, slug and name.
create_bundleCreate a bundle from a human-readable name. The slug is derived unless you pass one.
pull_bundlePull artifacts from a bundle. Filter by types, a query substring, and limit (100 by default, 500 at most).
read_artifactRead one artifact by URL/id, a saved version, or optional checkpoint history (includeHistory).
create_artifactCreate an artifact from a name and content, optionally filed into a bundle.
update_artifactReplace an artifact’s content, against the expectedVersion you read.
file_artifactAdd an existing artifact to bundles, or remove it from them.
list_inboxList unarchived inbox notes, newest first.
pull_inboxThe same, and it can write the notes to disk instead of returning their bodies.
archive_inboxArchive inbox notes by id.
list_commentsEvery open comment people have left about the artifacts this repo receives.
comment_evidenceWhy Mossbear raised a finding list_comments handed you: its claim, the change it predicts if the guide is fixed, and the evidence for it: the sessions or turns it cites, a skill’s load counts, or the authoring check it failed.
importReaches the rules-import endpoint rather than the context client.
artifact_feedbackThe open comments on one artifact, or on up to 25 at once with artifactIds, and, for a guide, what grading found about it, including your newest failure-pattern analysis of it.

pull_bundle and pull_inbox take a writeToDir that puts artifacts on disk and returns the paths instead of the bodies. It must be inside the current working directory, and it must not be an agent-guidance directory (.claude/, .agents/, .cursor/), because files written there are re-imported as new guides. A file already at a destination path with different content is left alone and reported in skipped.

Updating an artifact you did not just read

Section titled “Updating an artifact you did not just read”

update_artifact requires expectedVersion, the version you last read for that artifact, which read_artifact or pull_bundle returns. Without it the write would go over whatever the artifact says now, including an edit someone made since you read it. When the artifact has moved on, the update is refused. Read it again, reconcile your edit with the current body, and update with the version returned by that read.

An update replaces content and nothing else: it takes no name, bundle or format, and the edit propagates to every bundle the artifact belongs to.

When you create an artifact out of other artifacts, cite them in sources, each as {artifactId, version?, contentHash?}. They are recorded on the first saved revision as the writer’s assertion, and a citation that pins both a version and a hash the server can match is recorded as verified. update_artifact takes sources too, but they describe only the revision that write creates: they are not inherited from the previous one, so omit them to record none.

Use Mossbear’s MCP tools or CLI when a user gives you an artifact link. The link identifies the artifact; browser interaction requires a separate request.

read_artifact({ artifactId: "<URL-or-id>" }) returns the current content and version. Pass version to read one saved snapshot, or includeHistory: true for checkpoint metadata with ids, hashes, authors, timestamps and nextCursor. Pass that cursor as before to read an older page. Snapshot content can be unavailable; that state is explicit. Current versions track concurrent edits, while history retains checkpoints, so saved numbers can have gaps.

update_artifact updates the same artifact with artifactId, content and expectedVersion. Under a bundle allowlist, direct reads and updates are refused, so use pull_bundle for reads. A scope refusal does not authorize switching to CLI or broadening the grant. Creating under an allowlist requires a permitted bundle; create_artifact without a bundle saves an unfiled artifact, which appears in the Library. A rejected update is not retried with a newer version automatically; read again and reconcile the changes first.

The terminal equivalents share the validated API client:

Terminal window
mossbear artifacts read '<URL-or-id>' --json
mossbear artifacts history '<URL-or-id>' --json
mossbear artifacts read '<URL-or-id>' --version 1 --json
mossbear artifacts update '<URL-or-id>' --file revised.md --expected-version 3

Use the current version returned by a read for the update, rather than copying an example number or guessing from saved history. URLs must belong to the configured dashboard. If a needed operation is unavailable, report that gap.

You cannot create a guide through create_artifact. Its format accepts only the creatable content formats, and guide is deliberately absent. Creating one would write a standalone row with no guide registration behind it, so the caller would get a success for something that is not a guide.

Updating an existing guide is different: that is addressed by its id alone and needs nothing from that list. Guides get into your account through mossbear link, and into a repo through the bundles they belong to.

Two more paths deliberately leave the context client. import reaches the rules-import endpoint, because a guide cannot ride create_artifact; list_comments reaches the comments endpoint, because it answers “what has been left for me” rather than “what is in this bundle” and so has no bundle to pull from.

The allowlist gates list_comments and comment_evidence. A note is about an artifact a bundle delivers, so a grant limiting which bundles are reachable limits which comments are. artifact_feedback is a direct artifact read, so like read_artifact it is refused under any allowlist.

Terminal window
mossbear mcp --bundles work,design --read-only

--read-only refuses every context-layer write — create, update, file, archive — regardless of what your config says.

--bundles is a comma-separated allowlist of bundle slugs. Unset, it falls back to mcp.bundles in your config, and then to every bundle. A grant matches a bundle by either of its addresses, its slug or its id, so an allowlist written one way does not refuse a tool call that addresses it the other.

The inbox is granted separately, by the token @inbox:

Terminal window
mossbear mcp --bundles work,@inbox

A slug is lowercase alphanumerics and hyphens starting with an alphanumeric, so @inbox can never name a bundle. That is the point. The inbox gate used to be spelled as the plain slug inbox, and inbox is an ordinary creatable slug, so a user who named a project bundle inbox and allowlisted it would have silently granted read and archive over every note on the account.

A server holding @inbox reads the inbox and nothing else. Bundles are reached only through a bundle grant.

One function writes every context byte to disk, which is what keeps a pull, a writeToDir and the sync reconciler from disagreeing about what landing an artifact at a path means. Before it, four writers held their own copies of those rules and the copies disagreed: a writeToDir that clobbered the repo’s own files, a pull that resolved a placement conflict by deleting it, and a cache record that discarded half a baseline.