Agents & MCP
Fork-per-attempt is the workload offshoot is built around: give each agent attempt its own real database — a copy-on-write fork, not a mock, not a re-seed — let it write with a stock SQLite client, then keep the attempt that passed and let the rest expire. This page covers the MCP server that puts fork/checkpoint/rollback in the agent's own hands, the daemon workflow that captures the agent's writes live, and where the safety rails actually are.
Wire it into your agent
Claude Code plugin (MCP server + a skill that teaches the loop + advisory hooks):
claude plugin marketplace add sricola/offshoot
claude plugin install offshoot@offshoot
Claude Code, MCP only:
claude mcp add offshoot -- offshoot -store ./.offshoot mcp
Cursor:
(the link opens Cursor's install page, which hands off to the app to install
offshoot mcp as a stdio server; the store resolves from OFFSHOOT_STORE or ./.offshoot).
offshoot mcp speaks the Model Context Protocol on stdio — no daemon
required for the baseline. The plugin's skill includes the
"rules-file snippet" below plus the six-step loop and
rules it's drawn from; its hooks only add context (they never block a
command), and they stay silent when offshoot is not installed.
The agent gets nine tools, each described so the model knows when to reach for it: fork before a risky migration, checkpoint when tests pass, roll back when they don't, diff two attempts to see what actually changed, promote the attempt that worked. See it work end to end in a real captured session: the MCP walkthrough.
Rules-file snippet
Drop this into the project's AGENTS.md or CLAUDE.md so any agent reads
the loop before it touches a database, MCP tools or not:
Database changes: this project uses offshoot. Before any schema migration,
bulk delete, or data experiment, fork the database (offshoot_fork) and work
in the fork's checkout; checkpoint when tests pass; roll back when they
fail; promote the fork that worked. Never run destructive SQL on main's
checkout directly.
This is the same text the Claude Code plugin's skill teaches automatically; an agent that indexes docs instead of reading a rules file can pull the same loop from https://sricola.github.io/offshoot/llms.txt.
The nine tools
Verified against internal/mcp/tools.go; branch defaults to "main"
wherever it's optional.
| Tool | Arguments | What it does |
|---|---|---|
offshoot_list |
(none) | List every database and branch, with head txid, checkpoints, and protected flags — the orient-yourself call |
offshoot_checkout |
database, branch? |
Materialize a branch to a local SQLite file and return the path to open |
offshoot_checkpoint |
database, name, branch?, meta? |
Name the current state so it can be rolled back to or forked from |
offshoot_fork |
database, new_branch, branch?, at?, ttl?, meta? |
Create an isolated branch from head, or from a checkpoint via at |
offshoot_rollback |
database, to, branch? |
Return a branch to a named checkpoint, discarding everything since. The branch's previous head is kept first as a TTL'd safety fork <branch>-pre-rollback (one per branch, replaced by the next rollback), named in the result's backup field — undo by promoting it back onto the branch |
offshoot_promote |
database, source, target, force? |
Repoint target at source's head — ship the winning attempt. target's previous head is kept as a safety fork named <target>-pre-promote — the undo handle the result names — but that fork always carries a TTL (24h by default) and is one rolling slot per target, replaced by the next promote onto that target, so the undo window closes when either happens |
offshoot_destroy |
database, branch, force? |
Permanently discard a branch and its checkout |
offshoot_touch |
database, branch?, ttl? |
Reset a fork's activity clock so its TTL does not expire mid-task; ttl sets or ("none") clears it |
offshoot_diff |
database, left, right, table?, full?, max_bytes? |
Per-table rows added/removed/changed (and schema changes) between two branches or checkpoints — decide which attempt to promote; full adds capped sqldiff SQL |
What the host sees: annotations and structured results
Every tool carries the MCP spec's behavior hints, set explicitly:
offshoot_list and offshoot_diff are read-only; checkout, fork,
checkpoint, and touch are non-destructive; rollback, promote, and
destroy are destructive, so a host that honors destructiveHint prompts
before them. Every successful result also returns structuredContent
(snake_case JSON:
txid, path, ttl, expires_at, backup, …) next to the prose, so a
harness reads the fields instead of parsing sentences. fork and
checkpoint accept meta (string→string, at most 32 keys) to tag a
branch or checkpoint with a run id, git SHA, or agent name.
Forks expire by default
Agent-initiated forks carry a TTL: offshoot_fork applies offshoot mcp -default-ttl (default 24h) to any call that omits its own ttl, so
a branch an agent forks and forgets becomes reap-eligible a day later
instead of leaking forever. An explicit ttl:"<duration>" always wins,
ttl:"none" always yields no TTL, and -default-ttl 0/-default-ttl none disables the default entirely. The fork tool's response echoes the
applied TTL and computed expiry, so both land in the agent's transcript.
offshoot mcp itself is not a daemon in the offshoot serve sense — it
never owns a live SQLite session — but it isn't daemonless for TTL
purposes either: pass -reap-every DURATION (default 60s; 0/none
disables it) and the MCP process runs its own background reap on that
cadence for as long as it's up, expiring TTL'd forks and self-healing any
stranded delete claim. It steps aside automatically when a real
offshoot serve daemon is reachable on the same store (never a second
writer against one store), logging once that it's deferring. Either way,
-reap-every only reaps — it runs no GC, so reclaiming an expired
branch's storage still needs offshoot gc (by hand) or a running
offshoot serve daemon's janitor.
The safety posture
Destructive tools honor the same protected-branch rules as the CLI:
main is protected by default, so an unforced offshoot_promote onto it
or offshoot_destroy of it is refused, and the refusal comes back to
the agent as the tool result rather than a transport error, naming
-allow-force as the way past it. The agent forks and experiments
freely; touching the branch of record is a human/harness decision by
default, not an argument the agent can supply its own way past.
Concretely: force: true in an offshoot_promote or offshoot_destroy
call is honored only when the MCP server itself was started with
-allow-force (off by default). Without it, a call against a protected
target is refused before any mutation, no matter what the agent passes —
refuseForceOnProtected fails closed even on a transient error reading
the branch's own protected flag, so a storage hiccup can't accidentally
let a force through. offshoot_destroy's live-lease bypass is gated the
same way. This makes force a permission boundary the human running
offshoot mcp controls at startup, not a speed bump the model can step
over by itself — an agent that hits the refusal is expected to do what
the tool description says: ask the human to promote or destroy from the
CLI (offshoot promote ... --force / offshoot destroy ... --force), or
work on a fork instead. See mcp-walkthrough.md
for this refusal firing for real, followed by an offshoot_diff call and
a human-run CLI promote.
Any branch, not just main, can be put under the same protection:
offshoot protect <db>[@branch] sets the flag (offshoot unprotect
clears it); it's a CLI-only verb by design; there's no offshoot_protect
MCP tool; flipping the flag doesn't touch the branch's TTL clock.
offshoot_rollback keeps its own undo point: before repointing, it keeps
the branch's previous head as a TTL'd shared safety fork,
<branch>-pre-rollback (one per branch — the next rollback of the same
branch replaces it, at least 24h TTL), and reports it back as backup in
the tool result. Whether the undo itself is agent-doable depends on the
same -allow-force gate above: for an unprotected branch, an agent can
undo the rollback itself — offshoot_promote with source: "<branch>-pre-rollback", target: "<branch>". For a protected branch
(main, typically), that promote would itself be refused unless this
server allows force, so the undo is the human's CLI promote instead —
offshoot_rollback's result says so explicitly when it applies, naming
the exact command (offshoot promote <db>@<branch>-pre-rollback --onto <branch> --force).
That safety fork is itself guarded the same way once the branch it was
taken from is protected: without -allow-force, an agent can't destroy
main-pre-rollback (or main-pre-promote), shorten/clear its TTL via
offshoot_touch, or offshoot_promote something onto it (that would
repoint — i.e. destroy — the very undo point the fork exists to
preserve). A second offshoot_rollback main is refused outright while the
fork from the first one still exists too, rather than silently replacing
an undo point the human may still need — ask the human to promote or
destroy it first. A plain offshoot_touch that only extends the fork's
life (no ttl argument) is never blocked this way.
Also true regardless of tools: the daemon's unix socket is mode 0600,
and one leased, epoch-fenced writer per branch means concurrent attempts
get isolated forks, never interleaved writes
(architecture).
Live capture: the daemon workflow
Bare offshoot mcp runs every tool at rest — checkpoints quiesce the
checkout and write a segment of the changed pages or a full snapshot (the
result's kind says which). For an agent writing continuously, put
a daemon session under it and the same tools ride live capture instead
(incremental flushes, no quiesce, writer never paused):
offshoot -store ./.offshoot init # once
offshoot serve -socket /tmp/o.sock & # holds leases, captures continuously
sleep 1
offshoot session open app -socket /tmp/o.sock # the harness-opened session
claude mcp add offshoot -- offshoot -store ./.offshoot mcp -socket /tmp/o.sock
# ... agent works ...
offshoot session close app -socket /tmp/o.sock
The rules, exactly as shipped:
- No MCP tool ever opens a session itself. That's a harness's job —
the SDKs,
offshoot session open, or your own loop. A bare tool call has no guaranteed teardown, and an MCP-opened session would leak its lease exactly the way TTLs exist to prevent (the design reasoning). - With a session open on the branch:
offshoot_checkpointflushes live through the daemon andoffshoot_checkoutreturns the session's live checkout path. offshoot_forkrides the daemon whenever one is reachable — session or not (an open source session is flushed first, so unflushed writes land in the child; the fork uses the daemon's-snapshot-everyshare floor and counts in its metrics).- With no reachable daemon, every tool works at rest — exactly as if the daemon didn't exist.
offshoot_rollback,offshoot_promote(itstarget), andoffshoot_destroyrefuse — even withforce, which has no effect on this particular refusal — whenever the daemon has any session open on the affected branch, because all three repoint or delete a ref out from under a session the daemon still owns. Close the session first and retry.offshoot_promote'ssourceis the one exception: an open session there doesn't block, but what gets promoted is the source's last-flushed head, not its unflushed writes.
Full flag-level detail: offshoot mcp in the
reference.
Beyond MCP
- Test/eval harnesses — the paved road for fork-per-test: pytest fixtures and a vitest/jest testkit, seed-once-fork-many, TTL hygiene, CI. The eval-harness tutorial.
- Python / TypeScript SDKs — thin clients over the daemon's lifecycle API for harnesses that open and close sessions themselves. See the Python SDK and TypeScript SDK.
- LangGraph —
OffshootSaveris a realBaseCheckpointSaverfor putting LangGraph's own thread state under offshoot; the core SDK'sThreadForkshelper instead keeps an existing checkpointer and maps each thread to a branch of the agent's separate application database (LangGraph integrations). - Other frameworks — the OpenAI Agents SDK, LlamaIndex, and CrewAI each get a short honest recipe: framework recipes, OpenAI Agents SDK.