Transparent repository encryption
Plaintext on disk.
Ciphertext in history.
git-secret installs four git hooks that quietly encrypt what you commit and decrypt what you check out — modern authenticated encryption, no server, nothing to remember to run. GPG is entirely optional, for teams who'd rather wrap the key to existing identities than pass around a raw one.
$ git secret init Installed hooks: pre-commit, post-checkout, post-merge, pre-push $ cat secrets/db.yaml password: hunter2 $ git commit -m "add db credentials" pre-commit: encrypted 1 file for commit $ git show HEAD:secrets/db.yaml RENCxchacha20poly1305·h␛T·?··E·קּ,[··S···En·· $ cat secrets/db.yaml password: hunter2
What's actually different
Six facts, not adjectives
No "secure" or "seamless." Here's what the tool does and how.
Automatic, not manual
pre-commit, post-checkout, post-merge, and pre-push do the work. There's no encrypt step to forget.
Modern authenticated crypto
XChaCha20-Poly1305 by default, AES-256-GCM available. This is what encrypts every file, whichever key backend you pick — GPG is never in that path.
Glob patterns, committed
A versioned .repo-enc.yml decides what's in scope. Everything outside the pattern is left alone.
Pluggable, optionally committed
A gitignored local file, an env var, or GPG — wrap the key to existing identities and commit the wrapped blob, no out-of-band transfer needed.
A backstop against --no-verify
verify and pre-push refuse to let plaintext that slipped past a bypassed hook reach a remote.
One binary, three platforms
Pure Go, no runtime dependency but git itself. Hooks ship as POSIX shell and PowerShell.
The mechanism
How it works
In the order git actually calls them.
pre-commit
Encrypts the staged content of each matched file and repoints the index at the ciphertext blob — your working-tree copy is never touched.
post-checkout / post-merge
Decrypts matched files that checkout or merge just populated with ciphertext, if a key is available. No key just means it stays encrypted — the checkout itself never fails.
pre-push
Runs the same check as verify against HEAD and blocks the push if a bypassed pre-commit ever let plaintext through.
First five minutes
Quick start
# writes .repo-enc.yml, generates a key, installs hooks
git secret init
git add .repo-enc.yml .gitignore
git commit -m "chore: configure repo-enc"
echo "password: hunter2" > secrets/db.yaml git add secrets/db.yaml git commit -m "add db credentials" # pre-commit encrypts what's staged; your working copy stays plaintext
A teammate cloning the repo gets ciphertext in their working tree — that's what's committed. They need the key transferred out of band before unlock can decrypt it for them.
Reference
Commands
| Command | Effect |
|---|---|
init [pattern...] | Bootstrap: write .repo-enc.yml (idempotent), generate a key if missing, install hooks. |
status | Show which matched files are plaintext vs encrypted right now. |
lock | Encrypt every matched file in place — end of session. |
unlock | Decrypt every matched file in place — start of session. Marks files skip-worktree so git status stays quiet while you view them. |
encrypt <path...> | Encrypt specific files in place. |
decrypt <path...> | Decrypt specific files in place. |
rotate-keys | Generate a new key and re-encrypt every matched file under it. |
verify | Check every matched file committed at HEAD is ciphertext. |
adduser [recipient] | gpg backend only — grant access cheaply (re-wraps the key, no file re-encryption). Omit the argument to pick interactively. |
removeuser <recipient> | gpg backend only — revoke access and rotate to a new key (the removed recipient already saw the old one). |
hook <name> | Internal — invoked by the installed hooks. |
version | Show version, commit, and Go runtime info. |
Set SECRETIZE_SKIP_HOOKS=1 (or run under CI=1) to make every installed hook exit 0 immediately.
Editing an unlocked file? Run lock before git add — recent git versions refuse a plain git add on a skip-worktree'd path outright. lock re-encrypts straight from the working tree and clears the flag, so git add/git commit work normally right after.
If a teammate changes a file you currently have unlocked, git pull will refuse with git's standard "local changes would be overwritten" error. Recovery (if you were only viewing, not editing): git secret lock, then SECRETIZE_SKIP_HOOKS=1 git checkout -- <path> to discard your view without the post-checkout hook immediately re-decrypting it, then git pull. See the README for the full explanation.
Configuration
.repo-enc.yml
Committed at the repo root — this is how a teammate's clone knows what to encrypt.
version: 1 patterns: - "secrets/**" - "*.secret.env" exclude: - "secrets/public/**" key_backend: gpg key_source: .repo-enc/key.gpg gpg_recipients: - AAAABBBBCCCCDDDD...
file 32-byte hex key at key_source, gitignored automatically by init. env key read from the env var named by key_source. init/rotate-keys print export VAR=<hex> once. gpg key wrapped to gpg_recipients, safe to commit -- only a matching GPG secret key can unwrap it.
git secret init --key-backend gpg picks a recipient interactively from your local GPG keys (or pass --gpg-recipient <fingerprint> directly). GPG operations need gpg-agent/pinentry, which isn't available in a non-interactive session — prefer file/env for CI.
A practical use case, not a second product
The same core, one Kubernetes Secret
A real Secret manifest is rarely one credential — it's a dozen unrelated ones (an OIDC client secret, payment gateway keys, a webhook signing key...) in one stringData map. Whole-file encryption means rotating any single one means decrypting all of them, and every re-encryption rewrites the whole file, so the diff never tells you which key actually changed. kubectl-secret is a companion kubectl plugin, built on this project's exact same crypto core and key backends, that encrypts one value at a time instead.
$ kubectl secret encrypt-value -f deploy/app-secret.yaml -k OIDC_CLIENT_SECRET "..." repo-enc:v1:UkVOQwEReGNoYWNoYTIwcG9seTEzMDVhrP5K... $ cat deploy/app-secret.yaml stringData: OIDC_CLIENT_SECRET: "repo-enc:v1:UkVOQwEReGNoYWNoYTIwcG9seTEzMDVhrP5K..." WEBHOOK_SIGNING_KEY: "repo-enc:v1:8f3Nk2QpL9xVrTcMh7bYs2..." DEBUG: "true" $ kubectl secret apply -f deploy/app-secret.yaml secret/app-credentials configured
Not per-file
Only the keys you actually encrypt change ciphertext — a plaintext DEBUG value sits right next to an encrypted OIDC_CLIENT_SECRET in the same map.
To the object, not just the file
Ciphertext is bound to the manifest's apiVersion/kind/metadata.name/namespace as well as the file and key — moving valid ciphertext onto a different object, or retargeting it to another namespace with -n, fails to decrypt instead of silently authenticating onto the wrong Secret.
apply/create pipe straight to kubectl
Decryption happens in this process only; the plaintext manifest goes to kubectl over stdin, never touching disk.
rotate-keys covers it too
Per-value manifests are re-encrypted right alongside whole files — key rotation isn't partial just because you're using this.
For a GitOps cluster, see GitSecret
Applying a per-value-encrypted manifest straight through a GitOps controller needs a trusted decrypt step in that path — GitSecret below is that step, natively: a controller, no sidecar, no manifest-generation hook to wire up by hand.
Opt a manifest in with k8s_secret_paths in .repo-enc.yml — see the README for the full verb reference.
Beyond the CLI
GitSecret: a native CRD, ciphertext inline, no clone at all
GitSecret is a custom resource with its own controller — modeled on Bitnami sealed-secrets' shape, built on this project's existing multi-recipient GPG cryptography rather than one controller keypair. Ciphertext lives inline in the object, delivered by whatever already applies your manifests — ArgoCD, kubectl apply — with no repo clone, no SSH transport, and no network hop anywhere in the decrypt path.
$ git-secret-seal --namespace myapp --name my-secrets \ --recipient <controller-fpr> --recipient <your-fpr> \ --from-literal API_KEY=... > gitsecret.yaml $ cat gitsecret.yaml apiVersion: git-secret.opscalehub.io/v1alpha1 kind: GitSecret spec: encryptedData: API_KEY: UkVOQwEReGNoYWNoYTIwcG9seTEzMDVh... $ kubectl apply -f gitsecret.yaml gitsecret.git-secret.opscalehub.io/my-secrets created
Zero network hops to decrypt
Reconciling a GitSecret never makes a network call, never clones a repository, never opens an SSH connection. The only inputs are the object itself and this controller's own GPG key.
Not one controller keypair
The content key wraps to as many GPG recipients as you want — the controller's own identity, and independently, any number of humans or backups. --rewrap re-encrypts only that wrapped key when a recipient is added or removed; every value's ciphertext is untouched, and a newly-added recipient decrypts independently, with no involvement from the key that did the rewrapping.
Bound to the exact object and key
Every value's authenticated data is its namespace/name/key — an entry copied into a different GitSecret, or a renamed one, fails to decrypt instead of silently applying somewhere it wasn't sealed for.
The CRD is the source of truth
Built on controller-runtime. A GitSecret created fresh owns its target Secret outright: delete one, the other goes with it.
Real code, really tested
Unit-tested against real GPG operations and a fake Kubernetes client, then verified end-to-end against a real cluster — CRD install, a real apply, the actual controller binary, a correctly decrypted Secret.
Early and not yet packaged as a Helm chart or container image — build git-secret-controller and git-secret-seal from source for now (go build ./cmd/git-secret-controller, go build ./cmd/git-secret-seal), and install the CRD from config/crd/bases/. Full usage and the --rewrap reference: README.
Get it
Install
Prebuilt binary
GitHub Releases — linux, macOS, and Windows, amd64 and arm64.
- Rename to
git-secret(git-secret.exeon Windows) and put it onPATH git secretthen works as a native git subcommand
Build from source
git clone https://github.com/OpScaleHub/git-secret.git cd git-secret go build -o git-secret . sudo mv git-secret /usr/local/bin/
- Go 1.25+, git
- Windows: build with
-o git-secret.exeexplicitly