wip: the RFC v0.1
Version control where "commit" doesn't mean "publish to everyone". This is the full system argument: the design, the decisions behind it, and everything we think can kill it. Read it hostile. The comment thread is on GitHub.
§1 problem
git has exactly one read permission: all.
Anything committed is readable by everyone with repo access, forever. Consequences teams have normalized:
- Security fixes developed in out-of-band private forks, patch-over-email, ceremony outside the VCS.
- Companies split monorepos for access control long after tooling solved size, then pay for it in dependency hell.
- Secrets can't live in the repo at all, so every team bolts on a parallel universe: Vault, SOPS, sealed-secrets, .env files passed over Slack. Config and the code that consumes it are versioned in different systems with different access models, and drift between them causes real outages.
- HR configs and unreleased-product code live in scattered side repos or, worse, leak into history.
Separately, git's daily UX carries 20 years of accumulated fear: staging area, stash, detached HEAD, rebase-conflict lockups, force-push, lost work. Jujutsu (jj) has already proven a better model. Nobody has combined it with cryptographic read scopes.
§2 the design
wip: a VCS built on a forked jj-lib (Apache-2.0, Rust), with:
-
Sealed changes
wip commit --seal sec-teamencrypts content, message, and paths. World sees an opaque blob in the DAG. Per-change data key, wrapped per recipient (HPKE). A salted plaintext commitment sits inside the hashed portion, sowip reveal= publish a 32-byte key: the change becomes world-readable with the same hash, no rewrite, no rebase, review threads intact. Coordinated vulnerability disclosure becomes a native workflow. -
Standing path scopes
For monorepos and secrets.
wip scope create /payments --readers payments-team. File deltas under the path auto-encrypt; the rest of the commit stays public. A metadata-only tier ("file changed, 40 lines") keeps builds, blame, and logs coherent for non-readers. Same mechanism gives secrets a legitimate home in the repo: encrypted config versioned in the same commits as the code that consumes it, readable only by the deploy scope. wip is not a runtime secrets manager (no dynamic leases, no runtime audit); it replaces the SOPS/git-crypt/.env layer and complements Vault above it. Pitch: one repo for the whole company, where reads have owners too. -
jj's proven model
Working copy is a commit (no staging, no stash), stable change IDs across rewrites, first-class conflicts (rebase never blocks), operation log with universal undo.
-
Coach mode
Git commands work as aliases, do the wip-native thing, and explain the difference in one line. Muscle memory converts one alias at a time.
wip init --coexistruns inside an existing git repo; for public work, colleagues, CI, and GitHub never notice. Sealed content surfaces to plain-git colleagues as LFS-style opaque pointer files, never as editable text. Adoption unit = one developer. -
SQLite storage, filesystem as adapter
Two databases:
repo.db(syncable, safe to replicate anywhere) andlocal.db(never syncs: worktree state, plaintext cache). Working copy is a pluggable adapter: native FS, virtual FS (ProjFS/FUSE/FSKit), headless (API-only, for CI and AI agents), WASM/OPFS later. Db-as-truth quarantines the cross-platform bug zoo (case collisions, unicode normalization, path limits, CRLF) to the checkout adapter: lossy mappings are detected and warned, never silently committed as repo truth. -
Server as optimization, never as trust root
Fully functional offline; any dumb remote (GitHub, S3) carries ciphertext. A server adds fetch ACLs (existence hiding), forward revocation, equivocation prevention, review UI, and lazy fetch at monorepo scale.
§3 how the keys work, in plain words
The whole crypto story is five moves. Three people for the walk-through: you, Alice (on your security team), and Bob (not on it).
-
Identity
You already have an SSH key; GitHub even publishes it at github.com/you.keys. wip uses it exactly once: to sign a fresh decryption key for each of your devices. The SSH key says who you are. The device key does the decrypting. Rotate your SSH key next year and nothing breaks, because access points at device keys, and those get re-signed by whatever SSH key replaces the old one.
A team like
payments-teamis a chain of signed grants. Everywip grant payments-team @aliceis a statement signed by someone who already reads it, chaining back to the creator. Resolving--readers payments-teamwalks that chain, fetches each member's SSH key, and wraps one copy of the scope key per signed device key. The honest hole: key discovery leans on GitHub, so an account compromise can enroll an attacker's device key. Pinning and cross-signing are planned hardening; the kill list already says so.$ wip grant payments-team @alice @bob # signed by you roster: 3 readers · 7 device keys $ wip id @alice # key discovery ssh-ed25519 AAAA… · via github.com/alice.keys · 2 device keys
-
Sealing
wip commit --seal sec-teamgenerates a random key for this one change, the data key, and encrypts the diff, the commit message, and the file paths with it. Then it encrypts a copy of that data key to every member of sec-team, one copy per device key. Those copies are called wraps. The sealed blob and its wraps travel through any remote, GitHub included, as ciphertext. -
Reading
Alice's wip finds the wrap addressed to her device key, unwraps the data key, decrypts the change. Bob has no wrap, so Bob has noise. No server checked a permission table; there is no table. He just doesn't have a key.
-
Granting
wip grant sec-team @carol: someone who can already read the scope wraps its key for Carol's devices. You can only hand out what you already hold, and that one sentence is the security model: no grant can ever exceed the granter. Works the same for a teammate, a CI job, or an agent with an 8-hour TTL. Revoking Carol stops all future wraps and rotates the key forward; it cannot take back bytes she already fetched. Culprit 2 below says that plainly, because overselling revocation is how you lose the security crowd. -
Revealing
The embargo ends:
wip reveal rk3vpublishes the change's data key itself, 32 bytes, for anyone to use. From the start, the change's hash covered the ciphertext plus a commitment to the plaintext, so decrypting it changes nothing about its identity. Same hash, no rewrite, review threads still attached. Disclosure day is publishing one key.
Everything else in the design is bookkeeping around these five moves.
§4 key design decisions
- Identity from SSH keys, decryption from dedicated keys. SSH keys (already deployed, published at github.com/user.keys) sign a per-device X25519 decryption key. Works with ssh-agent and hardware keys; SSH key rotation doesn't destroy access.
- Seal taint is viral. Squash/merge/rebase output label = join of input labels. Loosening only by explicit reveal/grant. The label rules are information-flow control, enforced by the tool rather than left to convention.
- Dedup sacrificed across seal scopes. Sealed chunks are addressed by ciphertext; convergent encryption across scopes would enable confirmation attacks. Storage cost accepted deliberately.
- Permission checks are cryptography, not if-statements. No wrap row = the bytes are noise to you.
- Exit is a feature, and a precondition. Published format spec, a standalone offline export/decrypt tool, and a git-flattening path must exist before anyone is asked to trust sealed history to wip. If the company dies, customers keep their bytes and their keys.
- Revert is the default state, not a command. Coexistence keeps the git projection valid continuously:
rm -rf .wip/and the repo is a plain git repo, was one all along.wip eject [--to git|jj]handles the lossy parts explicitly: unpushed changes become branches, op log is archived, stored conflicts materialize with markers, and sealed changes leave as ciphertext bundles plus your keys. Eject never declassifies team-sealed content. Eject to jj is near-lossless.
§5 culprits: what can kill this, stated plainly
- Adoption gravity. Git won on network effects; hg and bzr died anyway. Mitigation is the entire coexist/coach strategy, but it remains the #1 risk. wip must be adoptable by one engineer, invisibly, with zero team buy-in, or it dies.
- Revocation is forward-only. Crypto cannot retract bytes someone already fetched. Honest framing required: revoke = stop future fetches + rotate keys. Overselling this destroys credibility with the exact security audience the product courts.
- Metadata leaks. A sealed blob on a public remote leaks existence, size, timing, and recipient list; for an embargoed exploit fix, existence alone tips attackers. Requires a "dark" tier (keyholder-only sync) and discipline in defaults. Easy to get subtly wrong.
- Sealed content breaks consumers. Teammates can't build code they can't read; nobody can merge what they can't decrypt. Mitigations (metadata-only tier, scope boundaries aligned to build graph, awaiting-keyholder rebase states) are designed but unproven at scale.
- Fork maintenance. jj-lib is pre-1.0 and churning. Label propagation hooks cut below the pluggable-backend boundary, so wip carries a real fork. Strategy: keep it surgical, upstream generic hooks, accept the rebase tax.
- Key loss = data loss. Lose every wrapped copy of a data key and the sealed history is gone, permanently. Needs org escrow/recovery keys by default, which reintroduces trust decisions the product must surface honestly rather than hide. Secrets-in-repo raises these stakes further: an unreadable sealed scope containing production config is an outage, not just lost history.
- Auto-snapshot vs secrecy. A tool whose pitch is confidentiality must not auto-record .env into synced history. Paranoid ignore defaults, size guards, and undo coaching are mandatory, not polish.
- Crypto review burden. Commitments, HPKE envelopes, key rotation, convergent-encryption pitfalls: this needs professional cryptographic review before anyone trusts an embargoed 0-day or production secret to it. Budget for audits; "we rolled our own" is disqualifying in this market.
- Ecosystem surface. Hooks, IDE integrations, submodules, LFS-class files: each gap bounces a real team. Git interop covers most, headless API helps tooling, but the long tail is long.
§6 why now
Supermodel-class coding agents change two things: the multi-year build-a-VCS effort is compressible, and agents themselves are a new VCS client. Headless mode with crypto-enforced path scopes means an AI agent physically cannot read code it isn't granted, an answer to a question every engineering org is currently asking. jj's rise proves the market will move off git's UX; nobody has yet given it a reason to move off git's trust model.
§7 what wip is not
Not a blockchain, no tokens. Not end-to-end "zero-trust collaboration" theater: the server, when used, is honestly scoped. Not a runtime secrets manager: it versions encrypted config, it does not lease database credentials. Not a git killer by frontal assault: it lives inside git repos until it doesn't need to. And not a future paywall: the tool is free and stays free, whole, sealing included. The cryptography is never the thing you pay for.
§8 now break it
This design survived two rounds of hostile review before publication. The first, attacking the system itself, is published; the second attacked the adoption plan and stays private until there is something to adopt. The hardest open questions are on the front page, labeled. The best outcome is you finding the one we missed.