{"slug": "show-hn-offshoot-copy-on-write-branching-for-stock-sqlite-files", "title": "Show HN: Offshoot – Copy-on-write branching for stock SQLite files", "summary": "Offshoot, a new open-source tool, introduces copy-on-write branching for stock SQLite files, enabling AI agents and eval harnesses to fork databases instantly with minimal storage overhead. A shared fork of a 100 MB database adds only 377 bytes to the store, about 280,000 times less than a full copy, and every checkout is a plain .db file. The tool supports commands like create, fork, checkpoint, rollback, and promote, and includes a daemon mode and MCP support.", "body_md": "**Branch SQLite like git** — fork-per-attempt databases for AI agents and eval harnesses.\n\nCreate, fork, checkpoint, rollback, promote — as stock SQLite files, on your storage, with one binary.\n\n[Quickstart](#quickstart-60-seconds-no-server-no-bucket) · [Install](#install) · [Daemon](#daemon-mode) · [MCP](#mcp) · [SDKs](#python-sdk) · [Benchmarks](/sricola/offshoot/blob/main/docs/benchmarks.md) · [FAQ](/sricola/offshoot/blob/main/docs/faq.md) · [Roadmap](/sricola/offshoot/blob/main/ROADMAP.md)\n\n`377 B`\n\nper shared fork of a 100 MB database · `kill -9`\n\ndurable · every checkout a stock `.db`\n\nfile\n\nAn agent attempt or eval run needs a real database it can trash: mocks aren't real, re-seeding is slow, and container or VM snapshots version a whole machine to get at one file. offshoot branches the database itself — copy-on-write forks of stock SQLite files over a local directory or an S3-compatible bucket.\n\nA shared fork of a 100 MB database adds\n[377 bytes](/sricola/offshoot/blob/main/docs/benchmarks.md#added-object-store-bytes-per-fork-100-mb-database)\nto the store — about 280,000× less than a copy — and every checkout is a\nplain `.db`\n\nfile any SQLite tool opens. Try N migrations or N agent\nattempts on N forks, `promote`\n\nthe winner, and let the losers expire:\n\n```\n              fork ┌─ attempt-1 ●──✗            expires (TTL)\n                   │\n  main ●───────●───┼─ attempt-2 ●──●──✓   ──►   promote: main repoints here\n       seed    cp  │\n                   └─ attempt-3 ●──✗            expires (TTL)\n```\n\nNo merge, no conflict resolution — the winner is promoted whole and the\nlosers reap themselves. (That's a design position, not a gap:\n[what offshoot deliberately doesn't do](#what-offshoot-deliberately-doesnt-do).)\n\n```\ngo build -o offshoot ./cmd/offshoot\n./offshoot init\n./offshoot create app\nsqlite3 \"$(./offshoot checkout app)\" \"CREATE TABLE users (name); INSERT INTO users VALUES ('ada');\"\n./offshoot checkpoint app v1\n./offshoot fork app attempt-1        # instant branch\nsqlite3 \"$(./offshoot checkout app@attempt-1)\" \"DELETE FROM users;\"   # destructive experiment\n./offshoot rollback app@attempt-1 --to fork                        # undo it\n./offshoot promote app@attempt-1 --onto main --force               # or ship it\n./offshoot status\n```\n\nThat's most of the surface already. The full vocabulary, one line each\n(every command and flag: [docs/reference.md](/sricola/offshoot/blob/main/docs/reference.md)):\n\n| Command | What it does |\n|---|---|\n`create` / `checkout` |\nnew database / materialize a working copy — prints a plain `.db` path |\n`checkpoint` |\nsnapshot the checkout as a named, rollback-able point |\n`fork` |\nbranch from head or a checkpoint — instant, copy-on-write, optional `--ttl` |\n`rollback` / `promote` |\nrepoint a branch at a checkpoint / repoint a target at a branch's head |\n`diff` / `export` |\nsqldiff two branches or checkpoints / copy state out to a plain file |\n`destroy` / `gc` |\ndelete a branch / collect unreachable objects |\n`serve` / `session` |\nthe daemon: leases, live capture, flush-without-pausing (\n|\n\n`mcp`\n\n[below](#mcp))Runnable demo: [ examples/parallel-attempts/](/sricola/offshoot/blob/main/examples/parallel-attempts)\nforks a database three ways, races three migrations against the forks,\npromotes the one that's actually correct, and discards the other two —\n\n`./examples/parallel-attempts/run.sh`\n\n. Real recording:\n[(play locally with](/sricola/offshoot/blob/main/docs/demo/parallel-attempts.cast)\n\n`docs/demo/parallel-attempts.cast`\n\n`asciinema play`\n\n).## Transcript of that demo, from a real run — nothing doctored\n\n``` js\n==> building offshoot\n==> creating a database with some data\n    3 orders, checkpoint 'before-migration'\n==> keeping the pre-migration state on its own branch\n    forked 'pre-migration' from the 'before-migration' checkpoint — promote wipes main's own checkpoint history, so this fork is what actually survives\n==> forking three attempts (instant, no copy)\n==> running the migrations in parallel forks\n    attempt-1: FAIL\n    attempt-2: FAIL\n    attempt-3: PASS\n==> winner: attempt-3\n==> promoting the winner onto main\n    promoted\n==> discarding the losers\n==> main now has the migrated data:\n    id|total|total_cents\n    1|19.99|1999\n    2|8.70|870\n    3|4.35|435\n==> and the pre-migration state is still one command away, on its own branch:\n    offshoot checkout shop@pre-migration\n```\n\nBuilding an eval harness or a test suite around this instead of a one-off\nscript? [docs/eval-harness.md](/sricola/offshoot/blob/main/docs/eval-harness.md) is the paved road:\nseed once, fork per test, xdist/vitest parallelism, golden-file assertions,\nTTL cleanup, and a CI recipe — for Python (`offshoot.pytest_plugin`\n\n) and\nTypeScript (`testkit`\n\n) alike. `offshoot export`\n\ncopies a checkpoint out to\na plain file for handoff, and `offshoot diff`\n\nanswers \"what changed between\nthese two attempts\" — see [docs/diff.md](/sricola/offshoot/blob/main/docs/diff.md) and\n[docs/reference.md](/sricola/offshoot/blob/main/docs/reference.md).\n\n**Copy-on-write forks, measured.** A shared fork writes two tiny objects — 377 B for a 100 MB database, flat from 1 to 100 forks — and forking a named checkpoint takes ~9–12 ms whether the database is 12 MB or 1 GB. A diverging child pays only for the pages it changes (~776 B per single-row transaction). Numbers, method, and the honest caveats:[docs/benchmarks.md](/sricola/offshoot/blob/main/docs/benchmarks.md#copy-on-write-fork-cost-v02x).**kill -9 durable.** The torture harness runs a stock`sqlite3`\n\nCLI writer and`SIGKILL`\n\ns it mid-write on roughly half of every round, while bouncing the capture engine mid-traffic every 10th round; the replica must converge to byte-identical dump output after every round. A 300 s run is ~3,500 rounds — zero divergence — and it runs in CI on a nightly cadence:[docs/testing.md](/sricola/offshoot/blob/main/docs/testing.md#the-kill--9-torture-harness).**Stock everything.** A checkout*is*a SQLite file — no forked engine, no special VFS on the read path — and`offshoot export`\n\nmaterializes any branch or checkpoint to a plain`.db`\n\nwith zero ongoing relationship to the store. The exit hatch is`cp`\n\n, and the pre-1.0[stability contract](/sricola/offshoot/blob/main/docs/stability.md)guarantees any format break ships with a migration or a documented export path in the same release.**Agent-native.** MCP tools so the agent forks before risky work and promotes what passed (); TTL'd branches that reap themselves so a forgotten attempt doesn't leak; pytest fixtures and a vitest/jest testkit for fork-per-test isolation (`offshoot mcp`\n\n[docs/eval-harness.md](/sricola/offshoot/blob/main/docs/eval-harness.md)); a LangGraph companion and framework recipes ([docs/recipes/](/sricola/offshoot/blob/main/docs/recipes)).\n\n**No row-level merge.** The workload is fork-many-keep-one:`promote`\n\nthe winner whole, let the losers TTL away. Real merge would forfeit the single-fenced-writer invariant the safety story rests on — if you need it,[Dolt is built for that](/sricola/offshoot/blob/main/docs/faq.md#can-i-merge-two-branches).**No multi-writer branches.** Exactly one leased, epoch-fenced writer per lineage; two agents writing \"at once\" get two forks and a`promote`\n\n([why](/sricola/offshoot/blob/main/docs/faq.md#why-one-writer-per-branch)).**No managed service, no multi-node.** Your bucket, your binary, Apache-2.0; replication, failover, and the word \"cluster\" are explicitly out of scope for v1 ([non-goals](/sricola/offshoot/blob/main/ROADMAP.md#non-goals-v1),[why not Turso/LiteFS](/sricola/offshoot/blob/main/docs/faq.md)).\n\nMore \"why not X\" (Litestream, Dolt, Neon, plain `cp`\n\n):\n[docs/faq.md](/sricola/offshoot/blob/main/docs/faq.md).\n\n| Channel | How |\n|---|---|\nHomebrew |\n`brew tap sricola/offshoot https://github.com/sricola/offshoot && brew trust sricola/offshoot && brew install offshoot` — recent Homebrew requires the explicit `trust` for third-party taps; the formula lives in-repo at\n`Formula/offshoot.rb` |\nDocker |\n`docker run --rm -v offshoot-data:/data ghcr.io/sricola/offshoot:latest init` — images publish to GHCR on every tagged release; the store lives in the `/data` volume, so reuse `-v offshoot-data:/data` across commands (`… create app` , `… serve` , and so on) |\nPrebuilt binaries |\n`offshoot_vX_os_arch.tar.gz` (+ `.sha256` ) from the\n|\n`go install` |\n`go install github.com/sricola/offshoot/cmd/offshoot@latest` |\nFrom source |\nthe Quickstart above (Go 1.25+, cgo) |\n\nThe full guide — store setup, S3 configuration, the fail-closed probe:\n[installation](https://sricola.github.io/offshoot/docs/installation/).\n\nRequires Go 1.25+ and cgo to build, and the `sqlite3`\n\nCLI for tests. Linux\nand macOS only. **Windows:** use WSL2 — the Linux binaries, Docker image,\nand build-from-source all work there as-is. Native Windows is unsupported:\noffshoot leans on POSIX file semantics (unix sockets, POSIX locks) that\ndon't map cleanly to Windows\n([why](/sricola/offshoot/blob/main/docs/faq.md#why-no-windows-support)).\n\n**v0.2.9.** What's shipped and exercised by tests that would\nfail if it broke:\n\n- local and S3-compatible stores behind a shared conformance suite\n- copy-on-write forks; checkpoint / rollback / promote / export / diff\n- live WAL capture with incremental segments\n- leases with epoch fencing, and CAS on every ref update\n- TTL reaping and GC\n- the daemon, with metrics and events; an MCP server\n- Python and TypeScript SDKs with test fixtures\n\n[docs/status.md](/sricola/offshoot/blob/main/docs/status.md) is the honest per-feature accounting —\nshipped-and-tested vs. shipped-but-unverified vs. still on the\n[roadmap](/sricola/offshoot/blob/main/ROADMAP.md) — and [docs/testing.md](/sricola/offshoot/blob/main/docs/testing.md) shows the\nCI gates behind the \"tested\" column.\n\nThe caveats, stated plainly: the CLI surface and the on-disk storage\nformat may still change before 1.0 — but never silently. Every store\nrecords a layout version, and a binary that doesn't understand a store's\nlayout refuses the whole store rather than guessing (0.2.0's first\ncopy-on-write fork exercised exactly that gate for real — see\n[CHANGELOG.md](/sricola/offshoot/blob/main/CHANGELOG.md)). Any format break ships in the same release\nwith a migration or a documented `export`\n\n→ `create --from`\n\npath: the\n[stability contract](/sricola/offshoot/blob/main/docs/stability.md) is the full promise, including the\nproposed v1.0 criteria. 1.0 is reserved for the point the storage format\nfreezes.\n\n```\noffshoot -store ./.offshoot init                 # local directory (default)\noffshoot -store s3://my-bucket/offshoot init     # S3-compatible bucket\n```\n\noffshoot's safety rests on compare-and-swap: every branch ref update is a\nconditional write. At attach time it **probes the store** and refuses to\nrun if conditional writes are not enforced, rather than silently\ndegrading. That probe re-runs on every command (every CLI invocation\nattaches fresh) — fail-closed beats a cached \"it was fine last time\"; a\nlong-lived daemon (below) amortizes it across a session instead of paying\nit per command.\n\nConfiguration for `s3://`\n\nspecs — credentials come from the AWS SDK default\nchain (environment, shared config, IAM role):\n\n| Variable | Meaning |\n|---|---|\n`OFFSHOOT_S3_ENDPOINT` |\nCustom endpoint (MinIO, or any S3-compatible endpoint) |\n`OFFSHOOT_S3_REGION` |\nRegion; defaults to `auto` when an endpoint is set |\n`OFFSHOOT_S3_PATH_STYLE` |\n`1` for path-style addressing (MinIO) |\n`OFFSHOOT_CHECKOUTS` |\nWhere checkouts are materialized (remote stores) |\n\nA provider is listed as supported only after the conformance suite and CAS\nprobe pass against it for real (`make test-s3`\n\n) — the in-process fake used\nin unit tests proves nothing about a real provider.\n\n| Provider | Status |\n|---|---|\n| MinIO | verified in CI — the conformance suite runs against real MinIO[1] on every PR and push to main |\n| AWS S3 | verified — `TestS3RealProvider` (probe + conformance + multipart) passed against a real bucket (us-east-1, 2026-08-13) |\n| Google Cloud Storage (S3 interop) | unsupported — no conditional writes on the S3 API; the probe refuses it (\n|\n\n[1] `minio/minio:latest`\n\ndigest: `sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`\n\nCheckouts are always real local SQLite files; only the snapshots and refs live in the store.\n\nAt rest (no daemon running): checkpoints are full snapshots; checkout\npaths are fixed at `<store>/checkouts/{db}/{branch}.db`\n\n; operations\nrequire the checkout to be quiescent (no live writers). Daemon mode\n(below) layers live capture, incremental segments, and continuous\ndurability on top of the same commands.\n\nAt rest, every offshoot command opens the store, does its work, and exits — so a checkpoint has to quiesce the database. The daemon removes that constraint: it holds the branch under lease and captures every committed transaction while your agent keeps writing.\n\n```\noffshoot serve &                       # holds leases, captures continuously\nsleep 1                                # let the listener come up\nP=$(offshoot session open app)         # capture the checkout path once\nsqlite3 \"$P\" \"CREATE TABLE t (v); INSERT INTO t VALUES ('agent wrote this');\"\noffshoot session flush app v1          # durable in the store, writer never paused\noffshoot session status                # durable txid per session\noffshoot session close app             # releases the lease\n```\n\nEverything ongoing the daemon does is bounded by a flag — each unpacked in the sections below:\n\n`serve` flag |\nDefault | What it bounds |\n|---|---|---|\n`-flush-every` |\n`30s` |\nworst-case committed-but-unflushed work lost if the daemon dies (`0` : durability advances only on explicit `flush` ) |\n`-snapshot-every` |\n`16` |\nread cost — materializing replays one snapshot plus at most N−1 segments |\n`-reap-every` |\n`1m` |\nhow often the janitor reaps TTL-expired branches and runs the GC sweep (`0` disables it) |\n`-gc-grace` |\n`15m` |\nhow long a tombstoned lineage's storage sits before a later cycle deletes it |\n`-http` |\noff | a loopback, token-authenticated listener: metrics, RPC, events, pprof |\n`-ro-cache-budget` |\nunlimited | disk held by the read-only checkout cache (LRU eviction) |\n`-socket` |\nper-store path | where the unix socket (mode 0600) lives; `OFFSHOOT_SOCKET` works too |\n\n**Durability is explicit and reported.** Between flushes, writes are\ncommitted to SQLite but not yet in the store; `session status`\n\nreports the\ntxid each session is durable through. By default the daemon also ships\nevery open session's work on a timer:\n\n```\noffshoot serve -flush-every 30s        # the default; 0 disables it\n```\n\n`-flush-every`\n\nbounds how much committed-but-unflushed work is ever at\nrisk: worst case, a daemon that dies loses at most one interval's worth of\nwrites. `0`\n\nreturns to durability that advances only on explicit `flush`\n\n.\nThe cadence is daemon-wide, not per-session\n([docs/status.md](/sricola/offshoot/blob/main/docs/status.md)). A session that loses its lease is\nfenced and stops — it will not write under a dead epoch — and\n`session status`\n\nshows the error.\n\nThe daemon serves a unix socket (mode 0600) under your cache directory,\none per store; override with `OFFSHOOT_SOCKET`\n\nor `-socket PATH`\n\n(pass the\nsame to every `offshoot session ...`\n\ncommand). Daemon and agent must share\na kernel and a local filesystem: the checkout is a real SQLite file both\nprocesses open.\n\nA long-running writer — the daemon — claims a branch with a lease:\n\n```\noffshoot lease list\noffshoot lease acquire app@main --ttl 60s\noffshoot lease release app@main\n```\n\nAcquiring or reclaiming a branch **bumps its epoch**, and every object is\nwritten under the epoch current at the time. A writer that pauses, loses\nits lease, and later resumes writes into a superseded prefix that no ref\npoints at — it cannot corrupt the branch, and its garbage is collected\nwith the lineage. Expiry is wall-clock and advisory; the guarantee against\nan uncooperative writer comes from the epoch fence and ref\ncompare-and-swap, not from the clock\n([docs/testing.md](/sricola/offshoot/blob/main/docs/testing.md#fencing-and-cas-in-two-paragraphs)).\n`offshoot lease acquire`\n\nexits immediately, so its lease expires unless\nrenewed — it exists for inspection and for breaking a stuck lease.\n\nA branch can carry a TTL, set at fork time or any time after:\n\n```\noffshoot fork app attempt-1 --ttl 2h        # reap-eligible 2h after last activity\noffshoot touch app@attempt-1 --ttl 30m      # resets the clock, changes the TTL\noffshoot touch app@attempt-1                # resets the clock, TTL unchanged\noffshoot touch app@attempt-1 --ttl none     # clears the TTL\n```\n\nTTL is measured from the last durable write or lease renewal, whichever is\nlater. A branch with an active lease is never reaped — a daemon session\nholding a branch open keeps renewing, so the janitor can never reap a\nbranch it's actively writing to. Protected branches (`main`\n\n, by default)\nare never reaped regardless of TTL; branches without a TTL live until\ndestroyed. TTLs read back re-rendered through Go's canonical duration form\n(`--ttl 1h`\n\nreports as `ttl=1h0m0s`\n\n).\n\n`offshoot serve`\n\nruns the janitor — TTL reaping plus the periodic GC\nsweep — on an interval:\n\n```\noffshoot serve -reap-every 1m -gc-grace 15m   # both are the defaults\n```\n\n`-reap-every 0`\n\ndisables the janitor entirely (GC stays available on\ndemand via `offshoot gc`\n\n); `-gc-grace`\n\nis how long a tombstoned lineage's\nstorage sits before a later cycle actually deletes it.\n\nA daemon flush writes only the pages that changed since the previous\nflush. Every sixteenth flush writes a full snapshot instead, so\nmaterializing a branch never replays an unbounded chain: a read applies\none snapshot plus at most fifteen segments. `offshoot serve -snapshot-every N`\n\ntunes that cadence (default 16) — lower N means\ncheaper, more tightly bounded reads; higher N amortizes the full-snapshot\nupload across more flushes — see `offshoot serve`\n\n's entry in\n[docs/reference.md](/sricola/offshoot/blob/main/docs/reference.md) for the full trade-off. An idle\nsession — nothing committed since the last successful flush — skips the\ntick entirely, so a quiet session pays nothing; cost scales with what the\nagent actually writes, not wall-clock time.\n\nA session whose checkout had to be (re)materialized at open pays one\nsettling full-snapshot flush, once per session; a session reopened against\na clean, current checkout uploads nothing at all for it. The measurement\nand the exact suppression condition:\n[docs/benchmarks.md](/sricola/offshoot/blob/main/docs/benchmarks.md#settling-flush-cost-task-2-controller-decision)\nand `internal/session/session.go`\n\n's `rebaseline`\n\ndoc comment.\n\nThe at-rest `offshoot checkpoint`\n\nstill writes a full snapshot every time —\nit runs without a daemon, so it has no record of which pages changed. If\nyou checkpoint large databases in a loop, run a daemon.\n\nForking is a different cost from flushing — usually no storage cost at\nall. `offshoot fork`\n\nshares the parent's already-durable objects through a\nbase pointer: the child records where it forked from and writes new\nobjects only as it diverges, so N forks of a G-byte database cost\nnear-zero added store bytes rather than N×G, and reads stay bounded by\nconstruction. The asymmetry to know: **fork shares; promote,\nrollback, and compact each materialize a full independent copy**\n(measured numbers in\n\n[docs/benchmarks.md](/sricola/offshoot/blob/main/docs/benchmarks.md)). Destroying a parent stays instant, but its bytes are reclaimed only once no surviving shared child still reads through them —\n\n`offshoot compact`\n\ncuts that cord\non demand. The first shared fork bumps the store to layout version 2,\nwhich locks pre-copy-on-write binaries out of the whole store — the\nrefusal is the protection. Full model:\n[docs/reference.md](/sricola/offshoot/blob/main/docs/reference.md)'s\n\n`fork`\n\n/`compact`\n\n/`destroy`\n\nsections and\n[docs/operations.md](/sricola/offshoot/blob/main/docs/operations.md#storage-sharing-copy-on-write-forks); the storage-cost ledger, stated plainly:\n\n[docs/faq.md](/sricola/offshoot/blob/main/docs/faq.md#storage-cost-honestly).\n\n`offshoot serve -http ADDR`\n\nstarts a loopback-by-default,\ntoken-authenticated HTTP listener alongside the unix socket:\n`GET /metrics`\n\n(Prometheus text exposition, zero new dependencies),\n`GET /healthz`\n\n, `POST /rpc`\n\n(the same protocol the socket speaks),\n`GET /events`\n\n(Server-Sent Events for flush/fork/reap/eviction/fencing),\nand token-gated `GET /debug/pprof/*`\n\n. Six computed branch states answer\n\"what's this branch doing right now\", and `-ro-cache-budget`\n\nbounds the\nread-only checkout cache with LRU eviction. All of it is single-node:\n[docs/operations.md](/sricola/offshoot/blob/main/docs/operations.md) has the metrics reference, states\ntable, event schema, budget mechanics, and the HTTP threat model in one\nplace; [docs/recipes/kubernetes.md](/sricola/offshoot/blob/main/docs/recipes/kubernetes.md) has a real\nsidecar manifest.\n\nAn open session's FD footprint is small and fixed. Disk is the sharper\ncost: `Checkout`\n\nreuses a checkout that's already clean and current at the\nbranch's head instead of re-materializing it, so a daemon that keeps\nreopening the same untouched branch stays flat. A checkout that *does* get\nre-materialized (dirty, stale, or destroyed while an earlier descriptor\nstill points at it) strands one descriptor — and the disk behind it — for\nthe life of the daemon process; restarting the daemon reclaims everything.\nThe tradeoff to know: a clean-and-current checkout is served straight from\ndisk without consulting the store's chain. Full mechanics and caveats:\n[docs/operations.md](/sricola/offshoot/blob/main/docs/operations.md#budgets) and\n[docs/status.md](/sricola/offshoot/blob/main/docs/status.md)'s resource-behavior rows.\n\n**Read-only historical checkouts** (`offshoot checkout --at <checkpoint> --read-only`\n\n) live in a separate `checkouts-ro/`\n\ntree — one `chmod 0444`\n\nfile per `(db, branch, checkpoint)`\n\n, no sidecar, no lease, no stranded\ndescriptor — and **it is safe to rm -rf the entire checkouts-ro\ndirectory at any time**; the next call rebuilds what it needs from the\nstore.\n\n`offshoot export`\n\n's output has the same\nzero-ongoing-relationship property, written wherever you pointed it.Four ways to talk to offshoot: the CLI above needs no daemon and no SDK;\neverything below is a client of the daemon's lifecycle API and requires\n`offshoot serve`\n\nalready running. A fifth, operator-facing surface rides\nalongside without changing any of them: `serve -http ADDR`\n\nexposes the\nsame lifecycle API over HTTP (see\n[Metrics, HTTP, and events](#metrics-http-and-events)).\n\n| Surface | What it is | Daemon? |\n|---|---|---|\n|\n\n[MCP](#mcp)—`offshoot mcp`\n\n[Python SDK](#python-sdk)[TypeScript SDK](#typescript-sdk)[LangGraph companion](#langgraph)[HTTP](#metrics-http-and-events)—`serve -http`\n\n*is*the daemon`offshoot mcp`\n\nspeaks the Model Context Protocol on stdio, so an agent can\nbranch on its own initiative instead of asking you to run commands:\n\n```\nclaude mcp add offshoot -- offshoot -store ./.offshoot mcp\n```\n\nThe agent gets seven tools — list, checkout, checkpoint, fork, rollback,\npromote, destroy — described so it knows *when* to use them: fork before a\nrisky migration, checkpoint when tests pass, roll back when they don't,\npromote the attempt that worked. See it work end to end:\n[docs/demo/mcp-walkthrough.md](/sricola/offshoot/blob/main/docs/demo/mcp-walkthrough.md), a real\ncaptured session.\n\nDestructive tools respect the same protected-branch rules as the CLI: an\nagent can fork and experiment freely, but promoting onto or destroying\n`main`\n\nrequires an explicit force, and the refusal tells the agent so.\n\nAgent-created forks expire by default, so an agent that forks and forgets\ndoesn't leak branches forever: `offshoot_fork`\n\napplies `offshoot mcp -default-ttl`\n\n(default `24h`\n\n) to any call that omits its own `ttl`\n\n; pass\n`ttl:\"<duration>\"`\n\nto override, or `ttl:\"none\"`\n\nfor a branch that never\nexpires. The response echoes the TTL applied and the computed expiry, so\nboth are visible in the agent's transcript. **A TTL alone does not reap\nanything** — reaping is the janitor's job (`offshoot serve`\n\n), and\n`offshoot mcp`\n\nruns no daemon of its own; a daemonless MCP setup only\nsweeps expired branches when `offshoot gc`\n\nis run by hand.\n\n**MCP rides a running daemon when one is up.** `offshoot mcp`\n\nnever opens\na session itself — that's a harness's job (the SDKs, `offshoot session open`\n\n, or your own loop). With an open session on the branch:\n`offshoot_checkpoint`\n\nflushes live through the daemon (no quiesce) and\n`offshoot_checkout`\n\nreturns the session's live checkout path.\n`offshoot_fork`\n\nroutes through the daemon whenever one is reachable,\nsession or not (an open source session is flushed first, so an unflushed\nwrite always lands in the child). With no reachable daemon, every tool\nruns exactly as it does with no daemon at all.\n`offshoot_rollback`\n\n, `offshoot_promote`\n\n(checked against its `target`\n\n),\nand `offshoot_destroy`\n\ntake the opposite stance: each **refuses outright —\neven with force** — whenever the daemon has any session open on the\naffected branch, because all three repoint or delete a ref out from under\na session the daemon still owns; close the session first and retry\n(\n\n`offshoot_promote`\n\n's `source`\n\nis the one exception — an open session\nthere doesn't block, but the promoted state is the last-flushed head, not\nunflushed writes). Details:\n[docs/reference.md](/sricola/offshoot/blob/main/docs/reference.md).\n\n**In short: the good path for**\n\n`offshoot mcp`\n\nis a harness-opened session, opened before the agent's\ntool calls.`sdk/python`\n\nis a stdlib-only, thin client over the daemon's lifecycle\nAPI — it never opens SQLite itself and can't do anything the CLI can't; it\njust lets your process drive a running daemon instead of shelling out. Not\nyet published to PyPI — import it from a checkout of this repo:\n\n```\noffshoot -store ./.offshoot init\noffshoot serve -socket /tmp/o.sock &\npython\nimport sys; sys.path.insert(0, \"sdk/python\")\nimport offshoot\n\nwith offshoot.connect(\"/tmp/o.sock\") as c:\n    c.create(\"app\")\n    s = c.open(\"app\")              # sqlite3.connect(s.path); write; commit\n    s.flush(\"v1\")                  # durable in the store, writer never paused\n    c.fork(\"app\", \"main\", \"try\", ttl=\"2h\")\n    s.close()\n```\n\n`Client`\n\nalso exposes `branches()`\n\n, `dbs()`\n\n, `export()`\n\n, and\n`checkout_at()`\n\n(a read-only historical checkout).\n\n**Testing with pytest?** `pip install \"offshoot-db[pytest] @ git+https://github.com/sricola/offshoot#subdirectory=sdk/python\"`\n\n*(from the repo — not yet on PyPI)* registers\n`offshoot_daemon`\n\n/`offshoot_db`\n\n/`offshoot_fork`\n\nfixtures automatically —\nseed once, fork a fresh isolated branch per test, TTL-backstopped cleanup,\n`pytest-xdist`\n\nparallelism (one daemon per worker). Full tutorial:\n[docs/eval-harness.md](/sricola/offshoot/blob/main/docs/eval-harness.md); condensed reference:\n`sdk/python/README.md`\n\n.\n\n`sdk/typescript`\n\nis the same thin client, zero runtime dependencies. Also\nnot yet published to npm — build and import it from a checkout of this\nrepo:\n\n```\noffshoot -store ./.offshoot init\noffshoot serve -socket /tmp/o.sock &\n(cd sdk/typescript && npm install --no-audit --no-fund && npm run build)\njs\nimport { connect } from \"./sdk/typescript/dist/client.js\";\n\nconst c = await connect(\"/tmp/o.sock\");\nawait c.create(\"app\");\nconst s = await c.open(\"app\");     // sqlite3 s.path; write; commit\nawait s.flush(\"v1\");               // durable in the store, writer never paused\nawait c.fork(\"app\", \"main\", \"try\", { ttl: \"2h\" });\nawait s.close();\nawait c.close();\n```\n\n`Client`\n\nalso exposes `branches()`\n\n, `dbs()`\n\n, `export()`\n\n, and\n`checkoutAt()`\n\n— the same surface as the Python client above.\n\n**Testing with vitest/jest/ node:test?**\n\n`@offshoot-db/client/testkit`\n\n(`startDaemon`\n\n/`seedOnce`\n\n/`forkPerTest`\n\n/`dump`\n\n) is the framework-agnostic\ncounterpart of the pytest fixtures above. See\n[docs/eval-harness.md](/sricola/offshoot/blob/main/docs/eval-harness.md)'s TypeScript section and\n\n`sdk/typescript/README.md`\n\n.Both SDKs are exercised against a real daemon by `make test-sdks`\n\n(needs\n`python3`\n\nand `node`\n\n/`npm`\n\non PATH — not part of the default `make test`\n\n,\nwhich stays hermetic to the Go suite).\n\n`offshoot.langgraph.ThreadForks`\n\nis a checkpointer *companion*, not a\n`BaseCheckpointSaver`\n\n: it maps each LangGraph thread to its own offshoot\nbranch, so rewinding a thread to an earlier checkpoint and retrying also\nforks the *database* from that same point — the retry never inherits what\nthe original attempt wrote after it. See\n[ examples/langgraph-rewind/](/sricola/offshoot/blob/main/examples/langgraph-rewind), runnable with\n\n`python3 examples/langgraph-rewind/agent.py`\n\n— no server or bucket needed\n(it builds `offshoot`\n\nand starts its own private daemon).LangGraph is the one framework with a real companion package; everyone\nelse gets a short recipe instead of an adapter — see\n[docs/recipes/](/sricola/offshoot/blob/main/docs/recipes): Claude Code's MCP config and hooks pattern\n([claude-agent-sdk.md](/sricola/offshoot/blob/main/docs/recipes/claude-agent-sdk.md)), the OpenAI\nAgents SDK's `SQLiteSession`\n\npointed at an offshoot checkout path\n([openai-agents.md](/sricola/offshoot/blob/main/docs/recipes/openai-agents.md)), and short honest\nnotes on LlamaIndex and CrewAI\n([frameworks.md](/sricola/offshoot/blob/main/docs/recipes/frameworks.md)).\n\nRendered docs site: ** https://sricola.github.io/offshoot/docs/** — the\nsame canonical markdown as the in-repo links below, with a\ngetting-started track:\n\n[introduction](https://sricola.github.io/offshoot/docs/introduction/)·\n\n[installation](https://sricola.github.io/offshoot/docs/installation/)·\n\n[quickstart](https://sricola.github.io/offshoot/docs/quickstart/)·\n\n[core concepts](https://sricola.github.io/offshoot/docs/concepts/).\n\n**Understand it**\n\n[Architecture](/sricola/offshoot/blob/main/docs/architecture.md)— the storage model, chains, fencing, copy-on-write[FAQ](/sricola/offshoot/blob/main/docs/faq.md)— why not Litestream / LiteFS / Turso / Dolt /`cp`\n\n[Stability contract](/sricola/offshoot/blob/main/docs/stability.md)— pre-1.0 promises, v1.0 criteria[How offshoot is tested](/sricola/offshoot/blob/main/docs/testing.md)— torture numbers, CI gates[Benchmarks](/sricola/offshoot/blob/main/docs/benchmarks.md)— measured, with method\n\n**Use it**\n\n[Eval-harness tutorial](/sricola/offshoot/blob/main/docs/eval-harness.md)— seed-once-fork-many for pytest/vitest/`node:test`\n\n, install to CI[CLI reference](/sricola/offshoot/blob/main/docs/reference.md)— every command and flag[CI recipes](/sricola/offshoot/blob/main/docs/ci-recipes.md)— seed-once/fork-per-attempt Actions workflows[Framework recipes](/sricola/offshoot/blob/main/docs/recipes)— Claude Code hooks, OpenAI Agents SDK, LlamaIndex/CrewAI[Branch diff](/sricola/offshoot/blob/main/docs/diff.md)—`sqldiff`\n\nbetween branches and checkpoints\n\n**Operate it**\n\n[Operations](/sricola/offshoot/blob/main/docs/operations.md)— metrics, branch states, eventing, budgets, HTTP/auth threat model (single node)[Grafana dashboard](/sricola/offshoot/blob/main/docs/grafana-dashboard.json)— ready to import, all 18 metric families[Kubernetes sidecar recipe](/sricola/offshoot/blob/main/docs/recipes/kubernetes.md)\n\n**Track it**\n\n[Implemented/deferred status](/sricola/offshoot/blob/main/docs/status.md)— shipped-and-tested vs unverified, honestly labeled[Roadmap](/sricola/offshoot/blob/main/ROADMAP.md)·[CHANGELOG](/sricola/offshoot/blob/main/CHANGELOG.md)\n\n[CONTRIBUTING.md](/sricola/offshoot/blob/main/CONTRIBUTING.md)— dev setup and the test tiers, including`make ci-local`\n\n(mirrors CI's job matrix locally)[SECURITY.md](/sricola/offshoot/blob/main/SECURITY.md)— how to report vulnerabilities[CHANGELOG.md](/sricola/offshoot/blob/main/CHANGELOG.md)— release notes- License:\n[Apache-2.0](/sricola/offshoot/blob/main/LICENSE)\n\n**⑂** fork it, trash it, promote the one that worked", "url": "https://wpnews.pro/news/show-hn-offshoot-copy-on-write-branching-for-stock-sqlite-files", "canonical_source": "https://github.com/sricola/offshoot", "published_at": "2026-08-16 14:02:31+00:00", "updated_at": "2026-08-16 14:10:39.685312+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["Offshoot", "SQLite"], "alternates": {"html": "https://wpnews.pro/news/show-hn-offshoot-copy-on-write-branching-for-stock-sqlite-files", "markdown": "https://wpnews.pro/news/show-hn-offshoot-copy-on-write-branching-for-stock-sqlite-files.md", "text": "https://wpnews.pro/news/show-hn-offshoot-copy-on-write-branching-for-stock-sqlite-files.txt", "jsonld": "https://wpnews.pro/news/show-hn-offshoot-copy-on-write-branching-for-stock-sqlite-files.jsonld"}}