## Why encrypt files inside a repository?

For most secrets, the right answer is to keep them out of Git entirely — `.env` files excluded by [`.gitignore`](https://www.deployhq.com/git/ignoring-files), with the real values injected at deploy time. If that covers your situation, start with our guide to [keeping API keys out of your Git repository](https://www.deployhq.com/blog/protecting-your-api-keys-a-quick-guide) instead; it is the simpler and safer default.

But some things genuinely need to live in the repository:

- **Config that is version-controlled by nature** — encrypted values that must change in lockstep with the code that reads them, so a rollback restores the matching config.
- **Certificates, keystores, and signing keys** that a build step needs and that must be pinned to a specific commit.
- **Small teams without a secrets manager**, where standing up Vault or a cloud KMS is more operational overhead than the project warrants.
- **Repositories handed to clients or contractors**, where some files should be readable by everyone and others by a named few.

`git-crypt` handles exactly this case. It encrypts chosen files transparently: they are plaintext in your working directory and ciphertext in every commit, push, and clone. People without the key can still clone the repo, read the unencrypted files, and contribute — they simply see binary blobs where the secrets are.

## Is git-crypt the right tool?

| Approach | Secrets live in repo? | Best for | Main drawback |
|---|---|---|---|
| `.gitignore` + [DeployHQ config files](https://www.deployhq.com/support/config-files) | No | Almost everyone | Config not versioned with code |
| **git-crypt** | Yes, encrypted | Per-file access control; binary secrets | No key revocation |
| [dotenvx](https://www.deployhq.com/blog/how-to-deploy-php-applications-with-encrypted-environment-variables-using-dotenvx-and-deployhq) | Yes, encrypted | `.env` values specifically | Env vars only, not arbitrary files |
| Vault / cloud KMS | No | Larger teams, audit requirements | Real operational overhead |

The row that usually decides it: git-crypt encrypts **arbitrary files** — a `.p12`, a keystore, a service-account JSON — where dotenvx encrypts **environment variable values**. If your secret is not a key/value pair, git-crypt is the one that fits.

## Installing git-crypt

**macOS**

```bash
brew install git-crypt
```

**Debian / Ubuntu**

```bash
sudo apt install git-crypt
```

Debian stable currently ships 0.7.0; testing and unstable ship 0.8.0. If you need the newest release, build from source.

**From source**

```bash
git clone https://github.com/AGWA/git-crypt.git
cd git-crypt
make
sudo make install PREFIX=/usr/local
```

Build dependencies are `make`, `g++` (C++11, gcc 4.9 or newer), and `libssl-dev` (`openssl-devel` on RHEL/CentOS). At runtime git-crypt needs Git 1.7.2 or newer — 1.8.5+ is recommended — and OpenSSL.

Confirm the install:

```bash
git-crypt version
```

If you do not have Git set up yet, start with [how to install Git](https://www.deployhq.com/git/install-git).

## Initialising a repository

From the root of an existing repository:

```bash
git-crypt init
```

This generates a symmetric key and stores it at `.git/git-crypt/keys/default`. The key lives inside `.git/`, so it is **never committed and never pushed** — which also means a fresh clone cannot read your encrypted files until you explicitly share the key.

You can maintain several independent keys in one repository — useful when different groups should unlock different files:

```bash
git-crypt init -k clientkey
```

## Choosing which files to encrypt

git-crypt decides what to encrypt from `.gitattributes`. Create one in the repository root:

```
secretfile filter=git-crypt diff=git-crypt
*.key filter=git-crypt diff=git-crypt
secretdir/** filter=git-crypt diff=git-crypt
```

The pattern syntax is the same as `.gitignore`, so the rules covered in [ignoring unwanted files](https://www.deployhq.com/git/ignoring-files) apply here too.

> **This is the one mistake that matters.** Upstream puts it plainly: *"Make sure your .gitattributes rules are in place before you add sensitive files, or those files won't be encrypted!"* git-crypt encrypts on the way into a commit. A secret committed before the rule existed is sitting in your history in plaintext, and adding the rule afterwards does not retroactively encrypt it. If that has already happened, treat it as a leak: rotate the credential first, then clean the history using the approach in [removing large files from Git history](https://www.deployhq.com/git/removing-large-files-from-git-history).

Note also that `.gitattributes` itself must not be encrypted — Git needs to read it to know what to decrypt. Keep it in plaintext and protect it in review: anyone who can quietly delete a line from it can cause the next commit of that file to land unencrypted.

## Verifying what is actually encrypted

Never assume the rules matched. Check:

```bash
git-crypt status
```

Useful flags:

| Flag | Effect |
|---|---|
| `-e` | List only encrypted files |
| `-u` | List only unencrypted files |
| `-f` / `--fix` | Re-stage files whose encryption state does not match the current rules |

`git-crypt status -e` before your first push is the check worth building a habit around. You can also confirm from the other direction — ask Git what the committed blob actually looks like:

```bash
git show HEAD:secretfile | head -c 16 | xxd
```

An encrypted blob begins with git-crypt's magic header rather than your plaintext.

## Sharing access with your team

### Symmetric key

The blunt approach: export the key and hand it over through a channel that is *not* the repository.

```bash
git-crypt export-key ~/secret-repo-key
```

The recipient clones as normal, then:

```bash
git-crypt unlock ~/secret-repo-key
```

This is fine for one or two people. It scales badly: every holder has the same key, and there is no way to take it back from one person without rotating for everyone.

### GPG collaborators

The better option for a real team. Add someone by GPG user ID:

```bash
git-crypt add-gpg-user user@example.com
```

This commits a copy of the repository key encrypted to that person's GPG public key, under `.git-crypt/`. They then clone and run:

```bash
git-crypt unlock
```

No key file changes hands — git-crypt finds the GPG-encrypted copy in the repo and decrypts it with their private key.

| Flag | Effect |
|---|---|
| `-k KEY_NAME` | Add the user to a named key rather than the default |
| `-n` / `--no-commit` | Stage the change without committing it |
| `--trusted` | Accept the GPG key even if it is not marked trusted in your keyring |

## Locking and unlocking

```bash
git-crypt lock     # re-encrypt the working directory
git-crypt unlock   # decrypt using GPG
```

`lock` is worth knowing about for practical reasons rather than security ones: locking before you share your screen, hand over a laptop, or point a backup tool at the directory means the plaintext is not sitting on disk.

## What git-crypt does not protect

This is the section most tutorials skip, and it is the part that determines whether git-crypt is safe for your use case. Upstream is explicit about the limits.

**Not encrypted at all:**

- **File names.** A file called `production-aws-credentials.key` advertises itself perfectly well while encrypted.
- **Commit messages.** "fix prod db password" leaks the same information the file does.
- **Symlink targets, gitlinks, and other metadata.**

**Leaked even for encrypted files:**

- **File length**, approximately.
- **Whether two files are identical** — encryption is deterministic, which is what lets Git diff and merge these files at all, but it means identical plaintext produces identical ciphertext.

**Operational limits:**

- **There is no key revocation.** This is the big one. If someone leaves the team, you cannot un-share the key. They keep the ability to decrypt every version of every file they ever had access to, including history. Genuinely removing their access means rotating the underlying secrets and rewriting history — so plan for rotation from the start rather than treating it as an emergency procedure.
- **Encrypted files do not compress**, so a repository heavy with encrypted content grows faster than you would expect.
- **`git apply` will not work** on patches touching encrypted files unless the patch itself is encrypted.
- **Some third-party Git GUIs** do not handle git-crypt's filters reliably. Upstream flags this directly; verify your team's tooling before committing to it.

The practical reading: git-crypt is good at *confidentiality of file contents against someone who has the repository*. It is not an access-control system, and it is not a substitute for a secrets manager when you need revocation or an audit trail.

## Deploying a repository that uses git-crypt

Here is the part worth thinking through before you adopt it. A deployment system clones your repository — and a clone without the key contains **ciphertext**. Your build will happily copy an encrypted blob to production and your application will fail to parse it.

There are two honest ways to handle this, and for most teams it is the second.

**Unlock during the build.** If the secrets must be decrypted in the pipeline, you have to get the key into the pipeline — which means you have moved the problem rather than solved it, and you now have a key sitting in your CI configuration that itself has no revocation story. It is workable with a [build pipeline](https://www.deployhq.com/features/build-pipelines) step, but understand the trade you are making.

**Keep production secrets out of the pipeline entirely.** Use git-crypt for what it is genuinely good at — sharing secrets *between developers*, versioned alongside the code — and deliver production values separately through [DeployHQ config files](https://www.deployhq.com/support/config-files), which are stored encrypted and written to the server at deploy time without ever entering your repository. The two approaches complement each other rather than competing, and this combination avoids the bootstrapping problem completely.

For the wider picture on securing a release pipeline, see our [practical security tips for smarter deployments](https://www.deployhq.com/blog/protect-your-environments-practical-security-tips-for-smarter-deployments), and for `.env` handling specifically, [managing secrets with .env files and config](https://www.deployhq.com/blog/understanding-env-files-and-environment-variables).

## Quick reference

| I want to... | Command |
|---|---|
| Set up git-crypt in a repo | `git-crypt init` |
| Create a second, named key | `git-crypt init -k KEY_NAME` |
| Mark files for encryption | Add `path filter=git-crypt diff=git-crypt` to `.gitattributes` |
| See which files are encrypted | `git-crypt status -e` |
| Fix files whose state is stale | `git-crypt status -f` |
| Export the symmetric key | `git-crypt export-key FILENAME` |
| Unlock with a key file | `git-crypt unlock /path/to/key` |
| Unlock with GPG | `git-crypt unlock` |
| Add a collaborator by GPG ID | `git-crypt add-gpg-user USER_ID` |
| Re-encrypt the working tree | `git-crypt lock` |
| Check the installed version | `git-crypt version` |

Under the hood, git-crypt encrypts with AES-256 in CTR mode, using a synthetic IV derived from the SHA-1 HMAC of the file. That deterministic construction is what makes encrypted files diffable and mergeable in Git — and is also why identical files produce identical ciphertext.

---

Once your secrets are handled properly, [DeployHQ](https://www.deployhq.com) takes care of getting your code to your servers — with [config files](https://www.deployhq.com/support/config-files) delivering production credentials securely at deploy time, so they never touch your repository. [Start deploying for free](https://www.deployhq.com/signup).
