sops

SOPS encrypts values while leaving keys readable. Git diffs show which field changed without exposing secrets. Uses age, GPG, or cloud KMS.

Setup

install (Arch)
sudo pacman -S sops
set private key path (add to .zshrc)
export SOPS_AGE_KEY_FILE="$HOME/.secrets/.metadata/keys/master.age.key"
create .sops.yaml in repo root
creation_rules:
  - path_regex: data/.*\.sops\.yaml$
    age: >-
      age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn

The .sops.yaml only contains the public key — safe to commit.

Encrypt

create a file, then encrypt in-place
cat > data/d000/config.sops.yaml << 'EOF'
database:
    host: db.example.com
    username: admin
    password: supersecret123
EOF

sops --encrypt --in-place data/d000/config.sops.yaml
result — keys readable, values encrypted
database:
    host: ENC[AES256_GCM,data:...,type:str]
    username: ENC[AES256_GCM,data:...,type:str]
    password: ENC[AES256_GCM,data:...,type:str]
sops:
    age:
        - recipient: age1wtd...
    lastmodified: "2026-05-02T..."

Decrypt

decrypt to stdout (does not modify file)
sops --decrypt data/d000/config.sops.yaml
extract a single value by JSON path
sops --decrypt --extract '["database"]["password"]' data/d000/config.sops.yaml
use in an env var (same pattern as gopass)
export DB_PASS="$(sops --decrypt --extract '["database"]["password"]' data/d000/config.sops.yaml)"

Edit

opens in $EDITOR — decrypts on open, re-encrypts on save+quit
sops data/d000/config.sops.yaml

No manual decrypt/re-encrypt cycle. Just edit and quit.

age vs gopass vs sops

Tool Best for How it encrypts

age (encrypt-file)

Full files — prose, documents, recordings, AsciiDoc

Entire file → opaque binary blob. No readable structure.

gopass

Passwords and credentials with clipboard integration

GPG/age store. Interactive. gopass show -c for clipboard.

sops

Config files in git — YAML, JSON, ENV

Values only — keys stay plaintext. Git diffs are readable.

Decision rule: If you need to git diff the file and see what changed without decrypting — use sops. If the entire file is sensitive (legal docs, recordings) — use age. If it’s a password you access interactively — use gopass.

Configuration — .sops.yaml

Multiple rules (different keys for different paths)

creation_rules:
  # Personal files — age key
  - path_regex: data/d000/.*\.sops\.yaml$
    age: >-
      age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn

  # Work files — different key or KMS
  - path_regex: data/d001/.*\.sops\.yaml$
    age: >-
      age1differentkeyhere...

Rules are matched top-to-bottom. First match wins.

Unencrypted keys

By default sops encrypts all values. To leave specific keys unencrypted:

creation_rules:
  - path_regex: data/.*\.sops\.yaml$
    age: >-
      age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn
    unencrypted_suffix: _unencrypted

Then in your file:

config:
    host_unencrypted: db.example.com     # stays plaintext
    password: supersecret123              # gets encrypted

Troubleshooting

# "failed to load age identities"
# → SOPS_AGE_KEY_FILE points to wrong path
echo $SOPS_AGE_KEY_FILE
ls -la $SOPS_AGE_KEY_FILE

# "no matching creation rules"
# → file path doesn't match path_regex in .sops.yaml
# → check: does your filename end in .sops.yaml?
cat .sops.yaml

# Re-encrypt with updated .sops.yaml rules
sops updatekeys data/d000/config.sops.yaml

# Rotate data key (re-encrypts data key, not values)
sops --rotate --in-place data/d000/config.sops.yaml