Show HN: Offshoot – Copy-on-write branching for stock SQLite files 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. Branch SQLite like git — fork-per-attempt databases for AI agents and eval harnesses. Create, fork, checkpoint, rollback, promote — as stock SQLite files, on your storage, with one binary. 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 377 B per shared fork of a 100 MB database · kill -9 durable · every checkout a stock .db file An 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. A shared fork of a 100 MB database adds 377 bytes /sricola/offshoot/blob/main/docs/benchmarks.md added-object-store-bytes-per-fork-100-mb-database to the store — about 280,000× less than a copy — and every checkout is a plain .db file any SQLite tool opens. Try N migrations or N agent attempts on N forks, promote the winner, and let the losers expire: fork ┌─ attempt-1 ●──✗ expires TTL │ main ●───────●───┼─ attempt-2 ●──●──✓ ──► promote: main repoints here seed cp │ └─ attempt-3 ●──✗ expires TTL No merge, no conflict resolution — the winner is promoted whole and the losers reap themselves. That's a design position, not a gap: what offshoot deliberately doesn't do what-offshoot-deliberately-doesnt-do . go build -o offshoot ./cmd/offshoot ./offshoot init ./offshoot create app sqlite3 "$ ./offshoot checkout app " "CREATE TABLE users name ; INSERT INTO users VALUES 'ada' ;" ./offshoot checkpoint app v1 ./offshoot fork app attempt-1 instant branch sqlite3 "$ ./offshoot checkout app@attempt-1 " "DELETE FROM users;" destructive experiment ./offshoot rollback app@attempt-1 --to fork undo it ./offshoot promote app@attempt-1 --onto main --force or ship it ./offshoot status That's most of the surface already. The full vocabulary, one line each every command and flag: docs/reference.md /sricola/offshoot/blob/main/docs/reference.md : | Command | What it does | |---|---| create / checkout | new database / materialize a working copy — prints a plain .db path | checkpoint | snapshot the checkout as a named, rollback-able point | fork | branch from head or a checkpoint — instant, copy-on-write, optional --ttl | rollback / promote | repoint a branch at a checkpoint / repoint a target at a branch's head | diff / export | sqldiff two branches or checkpoints / copy state out to a plain file | destroy / gc | delete a branch / collect unreachable objects | serve / session | the daemon: leases, live capture, flush-without-pausing | mcp below mcp Runnable demo: examples/parallel-attempts/ /sricola/offshoot/blob/main/examples/parallel-attempts forks a database three ways, races three migrations against the forks, promotes the one that's actually correct, and discards the other two — ./examples/parallel-attempts/run.sh . Real recording: play locally with /sricola/offshoot/blob/main/docs/demo/parallel-attempts.cast docs/demo/parallel-attempts.cast asciinema play . Transcript of that demo, from a real run — nothing doctored js == building offshoot == creating a database with some data 3 orders, checkpoint 'before-migration' == keeping the pre-migration state on its own branch forked 'pre-migration' from the 'before-migration' checkpoint — promote wipes main's own checkpoint history, so this fork is what actually survives == forking three attempts instant, no copy == running the migrations in parallel forks attempt-1: FAIL attempt-2: FAIL attempt-3: PASS == winner: attempt-3 == promoting the winner onto main promoted == discarding the losers == main now has the migrated data: id|total|total cents 1|19.99|1999 2|8.70|870 3|4.35|435 == and the pre-migration state is still one command away, on its own branch: offshoot checkout shop@pre-migration Building an eval harness or a test suite around this instead of a one-off script? docs/eval-harness.md /sricola/offshoot/blob/main/docs/eval-harness.md is the paved road: seed once, fork per test, xdist/vitest parallelism, golden-file assertions, TTL cleanup, and a CI recipe — for Python offshoot.pytest plugin and TypeScript testkit alike. offshoot export copies a checkpoint out to a plain file for handoff, and offshoot diff answers "what changed between these two attempts" — see docs/diff.md /sricola/offshoot/blob/main/docs/diff.md and docs/reference.md /sricola/offshoot/blob/main/docs/reference.md . 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 CLI writer and SIGKILL s 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 materializes any branch or checkpoint to a plain .db with zero ongoing relationship to the store. The exit hatch is cp , 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 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 . No row-level merge. The workload is fork-many-keep-one: promote the 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 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 . More "why not X" Litestream, Dolt, Neon, plain cp : docs/faq.md /sricola/offshoot/blob/main/docs/faq.md . | Channel | How | |---|---| Homebrew | 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 Formula/offshoot.rb | Docker | 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 | Prebuilt binaries | offshoot vX os arch.tar.gz + .sha256 from the | go install | go install github.com/sricola/offshoot/cmd/offshoot@latest | From source | the Quickstart above Go 1.25+, cgo | The full guide — store setup, S3 configuration, the fail-closed probe: installation https://sricola.github.io/offshoot/docs/installation/ . Requires Go 1.25+ and cgo to build, and the sqlite3 CLI for tests. Linux and macOS only. Windows: use WSL2 — the Linux binaries, Docker image, and build-from-source all work there as-is. Native Windows is unsupported: offshoot leans on POSIX file semantics unix sockets, POSIX locks that don't map cleanly to Windows why /sricola/offshoot/blob/main/docs/faq.md why-no-windows-support . v0.2.9. What's shipped and exercised by tests that would fail if it broke: - local and S3-compatible stores behind a shared conformance suite - copy-on-write forks; checkpoint / rollback / promote / export / diff - live WAL capture with incremental segments - leases with epoch fencing, and CAS on every ref update - TTL reaping and GC - the daemon, with metrics and events; an MCP server - Python and TypeScript SDKs with test fixtures docs/status.md /sricola/offshoot/blob/main/docs/status.md is the honest per-feature accounting — shipped-and-tested vs. shipped-but-unverified vs. still on the roadmap /sricola/offshoot/blob/main/ROADMAP.md — and docs/testing.md /sricola/offshoot/blob/main/docs/testing.md shows the CI gates behind the "tested" column. The caveats, stated plainly: the CLI surface and the on-disk storage format may still change before 1.0 — but never silently. Every store records a layout version, and a binary that doesn't understand a store's layout refuses the whole store rather than guessing 0.2.0's first copy-on-write fork exercised exactly that gate for real — see CHANGELOG.md /sricola/offshoot/blob/main/CHANGELOG.md . Any format break ships in the same release with a migration or a documented export → create --from path: the stability contract /sricola/offshoot/blob/main/docs/stability.md is the full promise, including the proposed v1.0 criteria. 1.0 is reserved for the point the storage format freezes. offshoot -store ./.offshoot init local directory default offshoot -store s3://my-bucket/offshoot init S3-compatible bucket offshoot's safety rests on compare-and-swap: every branch ref update is a conditional write. At attach time it probes the store and refuses to run if conditional writes are not enforced, rather than silently degrading. That probe re-runs on every command every CLI invocation attaches fresh — fail-closed beats a cached "it was fine last time"; a long-lived daemon below amortizes it across a session instead of paying it per command. Configuration for s3:// specs — credentials come from the AWS SDK default chain environment, shared config, IAM role : | Variable | Meaning | |---|---| OFFSHOOT S3 ENDPOINT | Custom endpoint MinIO, or any S3-compatible endpoint | OFFSHOOT S3 REGION | Region; defaults to auto when an endpoint is set | OFFSHOOT S3 PATH STYLE | 1 for path-style addressing MinIO | OFFSHOOT CHECKOUTS | Where checkouts are materialized remote stores | A provider is listed as supported only after the conformance suite and CAS probe pass against it for real make test-s3 — the in-process fake used in unit tests proves nothing about a real provider. | Provider | Status | |---|---| | MinIO | verified in CI — the conformance suite runs against real MinIO 1 on every PR and push to main | | AWS S3 | verified — TestS3RealProvider probe + conformance + multipart passed against a real bucket us-east-1, 2026-08-13 | | Google Cloud Storage S3 interop | unsupported — no conditional writes on the S3 API; the probe refuses it | 1 minio/minio:latest digest: sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e Checkouts are always real local SQLite files; only the snapshots and refs live in the store. At rest no daemon running : checkpoints are full snapshots; checkout paths are fixed at