Recipe: Claude Code / the Claude Agent SDK
Two pieces: getting offshoot into an agent's tool surface (MCP), and wiring its branch lifecycle to session events (hooks) so a fork happens automatically at the start of a task and a checkpoint happens automatically at the end.
Every claim below is checked against what actually shipped — the MCP
tool descriptions, the daemon-mode reach documented in the README's
MCP section and docs/reference.md,
and this repo's own offshoot mcp command. The hooks pattern in the second
half is illustrative: it shows the shape of the wiring, but the exact
hook-event JSON schema is Claude Code's surface, not offshoot's, and can
change between Claude Code releases — verify the current shape against
Claude Code's own hooks documentation before shipping it, and treat the
JSON field names here as "this is the pattern," not a pinned contract the
way the rest of this repo's docs are.
MCP config
claude mcp add offshoot -- offshoot -store ./.offshoot mcp
This registers offshoot mcp as an MCP server over stdio (see
docs/reference.md's offshoot mcp entry) — no daemon
required to get seven tools into Claude's tool list: offshoot_list,
offshoot_checkout, offshoot_checkpoint, offshoot_fork,
offshoot_rollback, offshoot_promote, offshoot_destroy. Each tool's
description tells Claude when to reach for it (fork before a risky
change, checkpoint when something works, roll back when it doesn't), so a
bare claude mcp add is enough to get an agent branching on its own
initiative in an at-rest workflow — no hooks needed for that baseline case.
One default worth knowing before you rely on this baseline: every branch
offshoot_fork creates gets a 24h TTL by default (offshoot mcp -default-ttl, itself defaulting to 24h), not "no TTL" — an agent that
forks and forgets is reap-eligible a day later rather than leaking the
branch forever. An explicit ttl argument on the offshoot_fork call
always overrides it (including ttl:"none" for no TTL); -default-ttl none on offshoot mcp itself changes the default for every fork that
doesn't specify one. TTL alone reaps nothing without a running janitor
(offshoot serve, or a manual offshoot gc) — see
docs/reference.md's offshoot mcp entry for the full
detail. This applies with or without the daemon-riding upgrade and the
hooks below; it's true of every offshoot_fork call in this at-rest
baseline case too.
The rest of this recipe is about a specific upgrade: making
offshoot_checkpoint and offshoot_fork ride live daemon capture
(incremental, no full-snapshot re-encode) instead of running at rest, by
making sure a session is already open before the agent's first tool call.
The MCP daemon-mode reality (read this before wiring hooks)
offshoot mcp never opens a session itself. This is not a limitation to
work around — it's how the tool avoids the exact leak class the whole
milestone-2/3 TTL and lifecycle work exists to prevent (see
docs/status.md's MCP rows and
ROADMAP.md's Milestone 3 note on session lifecycle). An
MCP open tool with no natural owner responsible for closing it would
recreate that leak; the design deliberately leaves session lifecycle to
something that reliably closes what it opens — a harness, not a bare tool
call.
What that means in practice, quoted from the README's MCP section:
MCP rides a running daemon when one is up, but only for a branch a session is already open on.
offshoot mcpnever opens a session itself... on every call,offshoot_checkpoint,offshoot_fork, andoffshoot_checkouteach check fresh whether the daemon has one open for the branch in question. If so:offshoot_checkpointflushes it live through the daemon (no quiesce, no full-snapshot re-encode, no lease collision)... Without an already-open session, every one of those tools runs exactly as it does with no daemon at all.
So the recipe's job is narrow and concrete: open the session yourself, before the agent's first tool call, using the SDK or the CLI — then let Claude ride it through MCP for the rest of the task. This is exactly the same shape the pytest fixture and TS testkit use for their own workloads (see docs/eval-harness.md) — a harness that owns open/close, with MCP (or the agent) only ever touching an already-open session.
Opening the session, then letting the agent ride it
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 # THIS is the harness-opened session
claude mcp add offshoot -- offshoot -store ./.offshoot mcp -socket /tmp/o.sock
claude "Fix the flaky test in tests/test_orders.py, then checkpoint when it's green"
Now every offshoot_checkpoint call the agent makes against app flushes
through the already-open daemon session — incremental, no quiesce, no
full-snapshot re-encode — instead of running the at-rest path. When the
agent is done, close the session the same way you opened it:
offshoot session close app -socket /tmp/o.sock
Equivalently, open the session from the Python or TypeScript SDK instead of
the CLI (see the README's Python SDK /
TypeScript SDK sections) — anywhere a
harness can hold a Client.open(...) call open for the duration of the
agent's task works. The MCP tool calls don't know or care which one opened
the session; they just check whether one is open.
This is the recipe's actual content. Everything below (the hooks pattern) is one way to automate "open before the agent starts, close after it's done" so you don't have to run those two extra commands by hand every time — it is not required to get the live-capture behavior above; the manual open/close shown here already gets you there.
Hooks pattern (illustrative)
Claude Code's hooks let you run a command at defined points in a session's
lifecycle — SessionStart when a session begins, Stop when Claude
finishes responding. The pattern this recipe wants:
SessionStart: fork a fresh branch frommainand open a daemon session on it — the harness-opened session the section above requires.Stop(task completion): checkpoint the branch through the now-open session.- On failure (however your setup detects it — a failing test, a nonzero exit from your own verification step): close the session first, then roll back — rollback repoints the branch at a new lineage, so the daemon session must not still be open on it (the daemon's own rollback op refuses outright in that case, and an at-rest rollback would fence the still-open session out from under the daemon).
A minimal .claude/settings.json sketch — verify the exact hook-event
names and the JSON your Claude Code version passes on stdin/expects on
stdout against Claude Code's own hooks documentation before using this;
this is the shape of the wiring, and the illustrative caveat at the top of
this doc applies specifically here:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "scripts/offshoot-session-start.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "scripts/offshoot-session-stop.sh"
}
]
}
]
}
}
scripts/offshoot-session-start.sh — fork a branch named for this session
and open it:
#!/usr/bin/env bash
set -euo pipefail
BRANCH="agent-$(date +%s)"
# fork is an at-rest command and takes no -socket flag: it reads the durable
# ref only, so it's safe to run alongside the daemon. Only the session
# subcommands (and mcp/serve) take -socket, and only AFTER the subcommand.
offshoot -store ./.offshoot fork app "$BRANCH" --ttl 2h
offshoot -store ./.offshoot session open "app@$BRANCH" -socket /tmp/o.sock
echo "$BRANCH" > .offshoot-current-branch
scripts/offshoot-session-stop.sh — checkpoint and close if your own
success check passes; close and then roll back otherwise:
#!/usr/bin/env bash
set -euo pipefail
BRANCH="$(cat .offshoot-current-branch)"
if make test >/tmp/offshoot-hook-test.log 2>&1; then
offshoot -store ./.offshoot session flush "app@$BRANCH" done -socket /tmp/o.sock
offshoot -store ./.offshoot session close "app@$BRANCH" -socket /tmp/o.sock
else
# Close BEFORE rolling back: rollback repoints the branch, and the open
# daemon session owns its checkout. rollback is an at-rest command with
# no -socket flag; every fork branch has an auto-created "fork"
# checkpoint to roll back to.
offshoot -store ./.offshoot session close "app@$BRANCH" -socket /tmp/o.sock
offshoot -store ./.offshoot rollback "app@$BRANCH" --to fork
fi
Note what this pattern is not claiming: there is no MCP-level "on tool
failure" event in this repo's own seven tools, and offshoot has no opinion
about how your harness decides a task succeeded — make test above is a
stand-in for whatever your project's own success signal is. The fork's
--ttl 2h is the backstop if the Stop hook never runs at all (a crashed
session, a killed process) — see the eval-harness tutorial's
TTL hygiene section for the same pattern
applied to test fixtures.
Without hooks: the good-path minimum
If hooks are more machinery than you want, the two commands under
"Opening the session, then letting the agent ride it" above are the entire
recipe — open before, close after, by hand or from a wrapper script around
claude itself. Hooks just automate that; they don't add anything MCP
itself doesn't already do once a session is open.