sdt keeps your code in superposition.
sdt grades your code. It runs the project's own check against a state, in a throwaway clone, and records the result. sdt green then returns the last state that passed.
For that to be useful, the states have to exist. So sdt captures your working tree continuously from sdt init. There is no staging area and no stash. You never run save to keep your work safe.
sdt runs beside git and pushes to GitHub. You can adopt it one step at a time.
Every version control system records what changed. No version control system records what worked. This is why git bisect exists. A commit is too large a unit to answer the question. You must search inside one commit after the failure.
Agents make this worse. A forty-minute agent run makes hundreds of intermediate states. None of these states is a commit. A failure that appears late has more states to blame.
Continuous capture gives you an address for each state. Grading gives you an answer for each state.
sdt also removes friction that git keeps. There is no staging area to manage. You do not stash to change branch. A reset does not destroy work. Binary files do not need a separate tool.
| Feature | What it gives you |
|---|---|
| Verified history | sdt runs your check against a captured state in a clone it then deletes. sdt green rewinds to the last state that passed. |
| Warranted verdicts | Each green result names who wrote the code and the check. It also names which changed files the check read. |
| Continuous capture | Starts at sdt init. sdt keeps every state of the tree as a moment. Each moment costs less than one kilobyte. You never type save to stay safe. |
| Content-addressed store (BLAKE3 + FastCDC) | Large files and binaries are first-class, deduped at the chunk level. No LFS. |
| Durable by default | An acknowledged write is on the device, not in a cache a power cut empties. The barrier is spent where a name is published, so ordinary object writes stay at the speed they were. sdt config durability fast trades it back. |
| Working copy is always a change | There is no staging area and no stash. sdt save names a boundary. It is not how sdt keeps your work. |
| Operation log | sdt undo and sdt redo across the whole repo. Nothing gets lost. |
| Instant copy-on-write worktrees | sdt work <dir> spins up a workspace in milliseconds (APFS clonefile, Linux reflink). sdt work list, status, merge, remove, and restore run its whole life from the repo it came from. |
| Stat-cache index | status and save skip re-hashing unchanged files (mtime/size/inode). |
| Three-way merge + resolve | Conflict markers, then sdt resolve <file> / sdt resolve --abort. |
| Resolutions that stick | A resolved conflict is recorded once the tree grades green. When the same hunk conflicts again, in a merge or a rebase, the recorded resolution is applied and named. sdt resolve --list / --forget <id>. |
| Stacked branches | sdt new off a feature branch stacks the new branch on it. sdt stack shows the levels with their grades; stack rebase, stack merge, stack squash work the whole stack as one reversible operation. |
| Absorb | sdt absorb folds working edits into the changes they belong to. |
| Probe any state | sdt probe @2h -- zig build runs any command against any captured state in a throwaway clone and streams its output; sdt probe @green..@ -- <cmd> bisects the moments between two states, several clones at a time. The worktree is never touched. |
| Send one state | sdt send @a3f91c ships one exact tree, its check, and its verdict. sdt get materialises it as a copy-on-write worktree and says whether sdt grade there reproduces the verdict. |
| Verdicts expire | A recorded verdict is kept for verdicts.retain (default 90d). A record another has superseded goes sooner, a green that aged out goes on time, and a red is never dropped for being old. |
| Garbage collection | sdt gc reclaims space from unreachable objects (--dry-run to preview). What a rewrite abandoned stays recoverable for gc.retain (default 30d) and is collected after; anything a branch points at is kept regardless of age. |
| Purge a path | sdt purge <path...> erases a path from every change, so sdt gc can reclaim what a mistakenly committed build directory holds. |
| Prompt provenance (opt-in) | Record which agent or prompt produced a change, stored in the repo. |
| Per-line blame | sdt blame <file> shows per-line authorship, including agent/prompt. |
| Scriptable output | sdt status --json and sdt log --json; sdt completions <fish|zsh|bash>. |
| Bidirectional git interop | Import and export full history, branches, and tags. Push and pull to GitHub. |
| Git LFS interop | Pointers resolve to real content on import and clean back to pointers on export, sharing .git/lfs/objects with git-lfs. |
| History you can edit | rebase, squash, split (by path or by hunk), reorder, amend --at, drop, take, move. Every one is reversible with sdt undo. |
| Converges without a service | The operation log is a DAG with a merge that cannot fail, so two machines reconcile through sdt sync, or through any dumb transport that moves files. |
| Live multiplayer | sdt mesh puts every peer in one room. Everyone is a writer, edits land on the others in milliseconds, and there is no server: peers find each other by broadcast and converge by merge. |
| Shared verdicts | A green earned on one machine answers on every machine, because a verdict is keyed by content and not by who ran it. |
| Sparse fetch and serve | Pull only the paths you need. A peer is just an object store, no forced server. |
| Thin clones | sdt clone --thin takes the whole history and only the current tree's content. A file from an older state arrives from a peer, a served repo, or the remote the first time it is read. sdt fetch --all fills it in. |
| Sealed secrets | Save your .env safely. Values are encrypted per-variable into a sealed tree entry; the plaintext is never an object and git never sees the path. Team access is a wrapped key, not a service. |
| Encrypted sharing | sdt send hands a repo to someone peer to peer, or as a link or file no host can read. |
sdt captures the working tree as a moment. A moment is a content-addressed state with a stable id, a timestamp, and a cause. A moment is not a commit. Moments never appear in sdt log. Each moment costs less than one kilobyte. sdt stores only what changed from the previous state, and a full keyframe every 200 moments.
sdt grades a moment in four steps. It makes a copy-on-write clone of your worktree. It changes the clone to the target state. It runs your check inside the clone. It deletes the clone. sdt does not change your worktree, and your terminal does not wait.
sdt keys each verdict by content. sdt therefore grades a given state one time only.
Capture starts at sdt init. You do not turn it on, and you do not run save to stay safe:
sdt init # capture starts here
# ... an agent runs for forty minutes and wrecks something ...
sdt back # it is still thereGrading is the part that runs your code, so it stays inert until you say what to run:
sdt setup # asks the three questions that matter
sdt config check "zig build test" # or set just that onesdt uses no daemon. On macOS, launchd already runs. Launchd watches the worktree and starts sdt only after a file changes. That sdt process does its work and stops. Idle CPU use is zero, because no process waits.
sdt watch does the same work in a terminal, if you prefer to see it.
Then the payoff:
sdt green # rewind to the last state that actually passed
sdt back 3 # rewind three moments
sdt rewind @2h # or @green~2, @yesterday, @a3f91c
sdt moments # what was captured, and what each one did
sdt doctor # what is on, what is degraded, and why
# including whether a log has holes, and how much history they coverA rewind destroys nothing. sdt captures the state that you leave before it rewinds. sdt undo reverses the rewind.
sdt never shows a green result alone. An agent can write both the code and the test. A green result then proves only that the agent agrees with itself. Each verdict therefore carries a warrant on three axes:
- Independence. Did one actor write both the code and the check? The verdict reads
independentorco-authored. - Relevance. Did the check open the files that changed? The verdict reads
relevance 5/5. - Discrimination. Would the check fail on the previous code? The verdict reads
discriminatingorvacuous.
sdt computes all three axes in milliseconds. It uses no model.
A warrant labels a verdict. It never blocks. sdt does not stop a push and does not fail a build. A signal that blocks becomes a target, and an agent then optimizes against it.
A warrant does not tell you if your design is good. It answers one narrow question. It tells you if the green result in front of you has value.
To stop capture, set moments.enabled = false. To stop grading, set checks.enabled = false. Either setting returns sdt to plain version control.
superdetermine does not replace git or GitHub, and adopting it is reversible. It sits next to your .git, and you decide how far to lean in:
- Keep committing and pushing with git as usual. superdetermine imports and exports full history losslessly, so you are never locked in.
- Or enable dual-write (
sdt config git-sync on --global) and everysdt savealso lands a normal git commit, so your team, GitHub, and CI keep working while you drive with sdt. - Run
sdt export <dir>to materialize a plain git repo at any time.
If superdetermine turns out not to be for you, your git history is right there, untouched.
With git-sync on, the colocated .git is a live projection of sdt, and it works in both directions. Any app that only speaks git (an editor's source-control panel, a desktop client, an agent that runs git) reads the projection as an ordinary repository, and what it writes there becomes sdt truth. A git commit is absorbed as an sdt change with the same message. A git switch or git checkout -b moves or creates the sdt branch. A git stash is kept as sdt objects. sdt notices on the next command, on the next background tick (the launchd agent watches .git as well as the worktree), or in sdt watch. Nothing is configured per app, and no git binary is run: sdt reads and writes the repository through libgit2. The policy itself (reading git refs, pairing branches, deciding what to absorb) lives in Apricot's git_facade and talks to sdt only through the adapter contract, so any version control system with an Apricot adapter gets the same facade.
sdt stays the authority. sdt switch moves git's HEAD with it. sdt undo on an absorbed commit takes it back out of git log (it stays in git's reflog). A git branch that diverged from sdt is left alone and reported by sdt doctor until you pick a side with sdt import . or sdt sync . --force.
Zed's DeltaDB also records the work between your commits, so the comparison is fair to make.
DeltaDB records what happened. It captures every operation, links it to the conversation that produced it, and replicates that to everyone in a thread. It does not run your check. Zed says so directly: git and CI stay for "running checks", and nothing in DeltaDB knows whether a given state built or passed. Review there means reading the diff.
sdt records what worked. The check runs, the verdict is stored against the content, and the warrant tells you whether that green result is worth anything. That is the whole difference.
The other difference is where your code lives. Delta stores your repository contents, your git history, and your uncommitted edits on Zed's servers, and needs an account. sdt has no account and no server. Sharing is peer to peer, and the optional relay is one you can run yourself and cannot read what passes through it.
Replication is the part where the two look closest and are not. Both put your work in front of your team as it happens. Delta does it through its servers, which is what makes a thread a thing you can be in. sdt mesh does it between the machines themselves, and what replicates is not only the work but the verdict on it: a green earned on one machine answers on all of them. There is nothing to be logged into and nothing to be on.
sdt init
sdt setup # name, email, and the command that says it works
sdt save -m "message" # sv snapshot the working tree (no add, no stash)
sdt status | sdt diff | sdt log # st | d | l
sdt show <ref> # sh what one change or moment contains
sdt cat <ref>:file.txt # one file as of any state, without moving the tree
sdt new feature # n branch and switch
sdt work ../agent-copy # wt instant worktree
sdt work list # every worktree, what it saved, what it has not
sdt work merge agent-copy # bring its saved changes back here
sdt work remove agent-copy # set it aside (refuses unsaved edits; restore brings it back)
sdt probe @green -- zig build # pb run anything against any state, in a clone
sdt probe @green..@ -- zig build test # bisect the moments in between
sdt send @a3f91c # hand someone one exact state, verdict included
sdt undo / sdt redo # u / r
sdt blame file.txt # per-line authorship + provenance
sdt absorb # fold edits into the changes they belong to
sdt gc # reclaim unreachable objects
sdt purge zig-out # erase a path from all history, then gc
Multiplayer:
sdt mesh open # start a room here, print its secret
sdt mesh join <secret> # join the room that secret names
sdt mesh # mp go live: everyone a writer, no server
sdt mesh status # what is configured, and what it means
sdt mesh leave # forget the secret
Stacked branches. One sdt undo reverses a whole stack operation:
sdt stack # sk this stack, base to tip, with each branch's grade
sdt stack add ui --on api # stack a branch on its parent
sdt stack remove ui # take a branch out of its stack
sdt stack rebase # replay every branch onto its rewritten parent, in order
sdt stack merge # merge the stack into the base branch, bottom up (--into <base>)
sdt stack squash [branch] # collapse one level's changes into one
Resolving conflicts once:
sdt resolve <file> # res mark a conflict resolved; recorded once the tree grades green
sdt resolve --list # every recorded resolution, and any waiting for a grade
sdt resolve --forget <id> # drop one
Editing history. Every one of these is reversible with sdt undo:
sdt rebase <ref> # rb replay this branch onto a new base
sdt squash [n] [--at <ref>]# sq collapse adjacent changes
sdt split <ref> -- <paths> # spl split a change, including one that is not the tip
sdt split <ref> --hunk src/a.zig:1,3 # or by hunk
sdt reorder 3 1 2 # ro permute the last N changes
sdt amend --at <ref> # am fold working edits into a named change
sdt drop <ref> # dr remove a change, keep its content in the tree
sdt point <ref> # pt move the branch tip anywhere
sdt take <ref> # tk copy a change from another branch onto this one
sdt move <ref> <branch> # mv move a change onto another branch, one undo for both
Working with git:
sdt clone <git-url> [dir]
sdt import <git-repo> / sdt export <git-repo> [branch] [--force]
sdt push [remote] [branch] # uses your existing git credentials
sdt push --require-green # a red or ungraded tree does not leave the machine
sdt attest [ref] # post the verdict, and its warrant, as a commit status
sdt attest puts the warrant where your team already looks. The status reads
green · independent · relevance 5/5 · discriminating, so a required check in
branch protection tells you what the green is worth, not merely that it is green.
Telling a coding agent whether its work passed:
sdt grade --json # one object, real exit codes: 0 green, 10 red, 11 ungraded
sdt hook install # prints a Claude Code Stop hook; it writes no settings file
Large files (Git LFS):
sdt lfs track "*.psd" # matching files export to git as LFS pointers
sdt lfs ls | sdt lfs status # what is tracked, and what is cached locally
sdt lfs fetch | sdt lfs push
sdt lfs env # endpoint, cache location, settings
On import, sdt resolves LFS pointers to the real bytes (from .git/lfs/objects, else the LFS batch API) and stores them chunked in its own object database, so sdt diff, sdt blame and sdt work all see real files. On export and push it writes the pointers back and uploads any objects the remote is missing. Set sdt config lfs-smudge off to keep pointers verbatim instead, sdt config lfs-url <endpoint> to override the endpoint, and sdt config lfs-upload off to skip uploads.
Settings have names, and sdt config on its own prints every one of them with its value and where that value came from:
sdt config # everything, grouped, with defaults
sdt config check # what this one is, and what it takes
sdt config check "zig build test" # set it for this repo
sdt config name "Ada" --global # set it for every repo on this machine
sdt config capture-every 3s # durations, percentages and on/off are understood
sdt config check --unset # back to the default
sdt setup # ask me the three that matter
A value is checked before it is written, so sdt config cut sometimes says what cut takes instead of storing a word nothing reads, and a misspelling suggests what it probably meant. Identity (name, email, default-branch) is written for the whole machine by default; everything else belongs to the repo. --global and --local override that either way.
On disk it is still key = value with dotted keys — .sdt/config in the repo, ~/.config/sdt/config for the machine — and those keys keep working on the command line too. sdt config --keys prints the mapping for when you are editing a file rather than typing a command.
.env is the file everyone gitignores and then mails around anyway. sdt saves it instead, sealed, with no extra file to keep track of.
sdt key new # k new your keypair, once per machine
sdt seal .env # sl .env becomes uncommittable from here on
sdt save -m "add config"
Your .env stays exactly where it is, in plaintext, for your app to read. It just stops being committable as plaintext. Each save records .env in the tree as a sealed entry: a typed entry whose content is the sealed form, one token per value, with the layout and comments kept:
DATABASE_URL=gr1:csEGYiVIFSnnLn4Q2oJv2TyekUkzChccyeVXlXtF3QQ8TrpS...
STRIPE_KEY=gr1:HJh0CycJJ4ssQq3epof75ooeomALvzNQPGUo_5gAK3M6FNWS3U...
The plaintext is never an object. Who can open the values lives beside the repo in .sdt/seal, as the repo key wrapped to each member, and travels with the native carrier on push and clone. The git projection omits the path in every form: a GitHub reviewer sees your code and no .env at all, sealed or otherwise. A clone that arrives without a carrier says so in one loud line, because the sealed entries are not there to be had.
Each value gets its own key derived from the variable's name and path, and the nonce comes from the plaintext, so an unchanged value re-seals to identical bytes and only real edits show up in a diff. The name and path are authenticated, so moving a STRIPE_KEY ciphertext onto the DATABASE_URL line fails to decrypt rather than quietly returning the wrong secret. What this reveals, in full: your variable names, how many there are, each value's length, and whether a value repeats at that same name. Nothing else.
Adding a teammate is a grant, not an account:
sdt key show # they run this, send you the string
sdt key add dana gr1lPATx6VZ... # you run this, then push
sdt unseal # they run this, and have .env
Switching branches writes the plaintext back out for anyone holding a key, and leaves the file alone for anyone who does not. A repo sealed with the older .sdtsealed sidecar converts itself the first time any seal command or save runs: the paths and grants move into .sdt/seal, the sidecar is deleted, and the command says so.
The repo key is wrapped separately to each member with X25519 and ML-KEM-768, so an attacker has to break both, so values committed today stay sealed against a future quantum computer. sdt rotate issues a new key and re-wraps it; the next save records every value under it. It also tells you the part software cannot do: someone you removed still holds the old key and every commit they already cloned, so rotate the underlying credentials too.
Two commands: sdt send gives a repo away, sdt get picks one up. With no flags, send is peer to peer over your local network.
sdt send -> 43-hydrant-hostel
sdt get 43-hydrant-hostel
The sender announces only a two digit slot number on the network. The words never leave either machine, so there is nothing on the wire to capture. Same wifi is all it needs: no relay, no account, no IP handed out, nothing uploaded.
When you are not on the same network, pick how it should travel:
sdt send --file repo.grb one sealed file, no network at all
sdt send --link ./out static files you upload anywhere
sdt send --relay host:port across the internet, via a meeting point
sdt get takes whatever came out of any of those: a code, a URL, or a file.
Every object is encrypted under a fresh key and stored under a blinded name. The key lives after the #, which by the URL spec is never sent in a request, so the host serves bytes it cannot read and its terms of service stop being a security question. Object contents are verified against their own hashes on arrival, so a hostile host cannot substitute anything either.
With --file, send the file and the key over different channels. Either alone is useless.
The key is never transmitted. Both sides derive it from the spoken code by PAKE (SPAKE2 over Ristretto255), so a relay watching the whole exchange gets nothing it can attack offline. that is exactly what makes three words safe here where three words in a URL would not be. A wrong guess costs an online attempt, and a code burns after five. Run a relay yourself with sdt relay (sdt rv); sdt serve --link <dir> hosts a --link export over HTTP.
The two layers compose the way you would want: share a repo and the recipient gets the sealed entries, still sealed, because they were given the share key and not the repo key. Code shared, secrets not, without remembering to scrub anything. Granting the secrets is a separate, deliberate sdt key add.
Git interop deliberately has no share layer: GitHub sees your code so review works, and only your values stay sealed.
Every command has a short alias: sdt st, sdt d, sdt sv, sdt sl, sdt rv. Run sdt help for the full table. Output is colored when stdout is a terminal and respects NO_COLOR.
A clone holds every chunk of every file in history. A thin clone holds the history and the content of the tree you stand on, and nothing older:
sdt clone --thin 10.0.0.5:7777 repo # from `sdt serve` there
sdt clone --thin ../repo # from a repo on this disk
sdt clone --thin repo.grb#k=... # from a bundle or a share link
sdt clone --thin https://github.com/you/repo.git
sdt get --thin 43-hydrant-hostel # the same, peer to peer
sdt mesh join --thin <secret> # the same, in a room
Changes, trees, manifests, moments, verdicts and the op-log all arrive, so sdt log, sdt moments and sdt green work as they do in a full clone. Only the chunks are lazy. Switch to an older branch, rewind, restore a file, probe a state, or grade a clone, and the chunks that state needs are fetched on the way, in one batch, with one dim line saying how many arrived and from where. The sources are tried in order: peers in the room, then sources (a host:port, a repo path, a share link), then the remote's carrier. Offline with nothing to fetch from, the command names the file and every source it tried, and exits 12.
sdt config thin on makes any clone thin: the next sdt gc drops chunks outside the current tree, but only ones a source is known to hold. sdt fetch --all goes the other way and turns thin off. sdt doctor shows how many chunks are held against how many the history references. A chunk that is not here is not garbage, it is remote: gc keeps every manifest that names it.
Everything above is one repo on one machine. sdt mesh is the same repo on several, converging while you work.
sdt mesh open -> gallery-cubic-flint-hammock-compass
sdt mesh # go liveWhoever is joining runs the other half:
sdt mesh join gallery-cubic-flint-hammock-compass
sdt meshThat is the whole setup. There is no account, no host, no invite, and no list to be on. Holding the secret is membership.
● in the room on port 63784, announcing every 250ms
room 9ddf9431, polling this tree every 25ms
→ 2 peers · 0.9ms away · 41 objects in, 12 out · 3 verdicts adopted
Everyone is a writer. This is not a live session with one driver and a read-only passenger — that is live.enabled, and it is a different feature for a different situation. Here each peer edits its own tree and the trees converge. Nobody hands anybody a token, and nobody waits.
It converges without asking anyone. The op-log is a DAG whose merge is total and deterministic down to the content hash, so two peers that have seen the same operations land on the same view without exchanging another byte. That property does not care whether there are two peers or ten, which is why the mesh needs no leader and no global order.
Same-path edits do not stop anyone. Where two people genuinely changed the same file, the result is a superposition rather than conflict markers: both whole versions are kept, the worktree materialises one, and every peer's tree still compiles and still grades. sdt super shows what is holding more than one version and sdt collapse keeps one.
Greens travel. A verdict is keyed by (tree, tier, command, inputs) and by nothing about the machine that produced it, so a check another peer already paid for answers here for the identical tree. On a team, or across a fleet of agents grinding the same tree, that is the compounding one. Turn it off with sdt config mesh-verdicts off.
One peer grades, not all of them. Sharing a verdict after the fact still means everyone paid for it. Peers rank themselves for each grading job by hashing the job against their own id, so all of them independently reach the same ranking having agreed on nothing, and the ones that lose wait for the answer instead of computing it again. Nobody is elected; there is only a number each peer can work out for itself. A peer that loses still grades if the answer does not arrive, on a wait derived from what that check has actually been seen to take, because a duplicated run costs CPU and a run that never happens costs the point of the tool. sdt config mesh-grade-claim off goes back to everyone grading.
Moments do not travel. Capture is per-machine and arrives by the hundred; your teammates want your changes, not your keystrokes.
Across networks broadcast does not reach, name a peer directly. Still no server — that is one machine's address, not a registry:
sdt mesh --peer 100.83.4.11:7788The secret is never transmitted, in any form. Both ends derive the channel key from it by PAKE, so a listener on the network gets nothing it can attack offline, and a wrong guess costs an online attempt. What is broadcast is a blinded room tag: peers recognise their own room, and anyone else learns only that some repo exists nearby.
It lives in .sdt/config, which is local. It is not committed and does not travel with the repo.
Adopting a peer's verdict means trusting a check you did not watch run. That trust is bounded by the same secret: the peers who can write into your verdict log are exactly the peers who could already push you a change.
A link is opened once, authenticated once, and reconciled once in full. After that it stays up and carries deltas, so an edit does not pay for a handshake. On a LAN the wire is sub-millisecond; what you actually wait for is the poll that notices your tree moved, which is 25ms by default and is the number to turn down:
sdt config mesh-every 10msThe reason this stays cheap as history grows is that a peer remembers which subtrees it has already seen whole, so working out what to send touches what changed rather than what exists. Gossip is O(new), not O(repo).
Grab a binary from Releases, or update in place:
sdt update # latest stable
sdt update --nightly # latest nightly build
Linux release binaries target musl and are fully static. They run without a system libc or libgit2. macOS release binaries bundle libgit2, but still use the system libraries provided by macOS. All release builds use Zig's baseline CPU rather than instructions specific to the CI runner.
Requires Zig 0.16.
zig build # native zig-out/bin/sdt, ReleaseFast
zig build test
zig build -Doptimize=Debug # slow binary, every safety check on
To reproduce the portable x86-64 Linux release build:
zig build -Dtarget=x86_64-linux-musl -Dcpu=baseline -Dstatic=true
Use aarch64-linux-musl for the arm64 Linux artifact. The macOS release targets
are x86_64-macos and aarch64-macos; they also use -Dcpu=baseline, without
-Dstatic=true.
Early and opinionated. Interfaces may still change.
Apache-2.0. See LICENSE.
