RD · Recipient + Dispatch
Keys on your machine, builds on GitHub, triggered by sgit push. The recommended combination.
Every developer has their own keypair. No decryption key ever lives on
GitHub. sgit push starts the build and sends the key material with it,
sealed, for that one build.
The recommended pair, and the only one where the two properties people usually want hold at the same time: per-person access and no long-lived decryption key on the platform.
1 · Create the token
Do this first. sgit init uploads the secrets your workflow reads, and without
a token it can set up everything except the half that makes builds run.
GitHub → Settings → Developer settings → Fine-grained tokens → Generate new
| Repository access | Only select repositories → this one |
| Contents | Read and write — so you can push |
| Actions | Read and write — so sgit push can trigger builds |
| Secrets | Read and write — so init can upload them |
Metadata: read is added for you. Give it an expiry you will notice — sgit doctor warns before it lapses.
export GITHUB_PAT=github_pat_…GH_TOKEN and GITHUB_TOKEN work too. If none is set and you are at a
terminal, init will offer to take it and store it for you.
2 · Seal the repository
From the root of an existing Git repository:
sgit rdrd is the shorthand for this pair, and takes every flag init takes.
Spelled out, it is:
sgit init --mode recipient --ci-mode portal-dispatchIt asks one question — the project name, defaulted from the directory — and does the rest:
Admin keypair generated.
Public key (committed to manifest): age1ph3c3y660cerffgujpfu28ja3rp0k77…
admin private key → $CODESEAL/demo/admin.key
Your identity: $CODESEAL/demo/identity.age
public key: age1wt06z4uzwknw7qxcz6g8trhjk2cmxwsavkwpxl24mtjh9q7c34nqq…
It is outside the repository on purpose — it can never be
committed by accident. Back up $CODESEAL and you keep the
ability to read everything you have encrypted.
Dispatch keypair (CI Mode 4):
private → $CODESEAL/demo/dispatch.key
public → committed to the manifest
CI runner keypair:
private → $CODESEAL/demo/ci-runner.key
public → registered as recipient `ci-runner`
sealed → .sgit/ci-runner.age (readable by every recipient)Four keys, none of them in your repository:
| file | what it is |
|---|---|
admin.key | the master key — adds and removes recipients |
identity.age | yours; what decrypts your files |
ci-runner.key | the runner's; rides inside each dispatch token |
dispatch.key | unwraps that token; uploaded to GitHub |
All 0600. A key in the working tree is one git add -A from being published,
so none of them are there.
And in the repository — init lists this too, because it is a lot to do
silently:
In the repository:
.sgit/manifest.yaml what is protected, and how
.sgit/recipients.yaml who can decrypt
.gitignore managed block — keeps plaintext out of commits
.gitattributes managed block — marks blobs binary, no diff/merge
.github/workflows/codeseal-portal-dispatch.yml the only workflow for this mode
build.sh your build steps go here
Git hooks installed (.git/hooks — never committed):
pre-commit refuses a commit that would include plaintext
pre-push re-checks before anything leaves the machine
post-checkout decrypts what the new branch protects
post-merge the same, after a merge brings new blobsPlus .sgit/ci-runner.age — the runner key wrapped to every recipient, which
is what lets a teammate trigger builds without anyone sending them a key.
3 · Check GitHub is ready
sgit ci verifymylife-inc/demo is ready to build.
It asks GitHub what it actually has: a working credential, and
CODESEAL_DISPATCH_PRIVATE_KEY present by name. That secret unwraps a dispatch
token and decrypts nothing on its own — which is why it can sit on GitHub
without giving anything away.
4 · Protect something
sgit add-sensitive src/private
sgit encryptDirectory selections expand into individual file entries, so what is protected stays explicit as the tree grows.
git status --shortThe plaintext is gitignored; .sgit/store/ holds the ciphertext.
5 · The everyday loop
sgit add -s src/private/payment.rs
sgit commit -- -m "protect payments"
sgit push origin mainsgit push: auto-dispatch firing for branch main…
sgit ci dispatch: queued build 018f2a…
view at https://github.com/mylife-inc/demo/actions
sgit push pushes ciphertext, then calls workflow_dispatch with a token
carrying the runner key, encrypted to the dispatch public key. The runner opens
it with the secret from step 2, decrypts, builds, and scrubs.
To fire on every push, set it once:
dispatch:
auto_on_push:
enabled: true
branches: [main]6 · Add a second developer
They clone and generate an identity:
sgit onboard --offlineIdentity written to $CODESEAL/demo/identity.age (mode 0600)
Public key: age16yxpfma9c70dt2g2szv4lzz4…
You add them:
export SGIT_ADMIN_IDENTITY=$(cat $CODESEAL/demo/admin.key)
sgit rekey add-recipient bob age16yxpfma9c70dt2g2szv4lzz4… developerEvery file's key is unwrapped with the admin key and rewrapped for Bob. The
ciphertext does not change — only the small stanza beside it. The runner key
is rewrapped at the same time, so Bob's first sgit push fetches it from the
repository:
Fetched the CI runner key → $CODESEAL/demo/ci-runner.key
Nothing was sent between you. Bob needs his own token from step 1 — his GitHub account, his permissions.
Removing him is the same in reverse, and takes the runner key with it.
What you end up with
| On GitHub | ciphertext, and one key that decrypts nothing by itself |
| On your machine | every key, in one backed-up directory |
| Adding a teammate | a rewrap; no shared secret changes |
| Removing one | the same, and their key stops working |
| To start a build | you, from a terminal, with a token |
See also
- PD — the same trigger model with one shared passphrase
- RS — per-developer keys, but builds start on their own
- CI/CD — every mode's message flow, drawn
- Security model — what this protects against, and what it does not
