SOPS (Secret OPerationS) is a tool for managing secrets — encrypting values
inside YAML/JSON/ENV/INI/binary files while leaving keys readable. This
package lets you open a SOPS-encrypted file with find-file and edit it
like any other buffer; encryption happens transparently on save.
(use-package sops
:ensure (:type git :host github :repo "djgoku/sops")
:init
(global-sops-mode 1))That’s it. No commands to bind, no special keystrokes.
Requires sops >= 3.9.0 (>= 3.13.0 recommended for inline YAML comment
preservation).
- Open a sops-encrypted file with
M-x find-file(orC-x C-f). The buffer shows decrypted contents and thesopslighter appears in the modeline. - Edit normally.
- Save with
C-x C-s. The file on disk stays encrypted.
To create a new encrypted file: M-x sops-find-file /path/to/new.enc.yaml.
The buffer is seeded with a SOPS example (mirroring upstream sops
<path>); edit it and save. Requires a reachable .sops.yaml (in the
path’s directory or any ancestor) with a creation_rules entry
matching the path.
If the path already exists, sops-find-file behaves identically to
find-file – the global mode decrypts sops files transparently.
| Variable | Default | Purpose |
|---|---|---|
sops-executable | "sops" | Path to sops binary |
sops-prefilter-regex | (see below) | Files matching trigger filestatus check |
sops-extra-decrypt-args | nil | Extra args inserted between decrypt and the trailing file path |
sops-extra-encrypt-args | nil | Extra args appended after --filename-override FILE in encrypt |
sops-input-type-overrides | nil | Alist of (REGEX . TYPE) for files where extension doesn’t tell sops the type |
The default sops-prefilter-regex is:
"\\.\\(ya?ml\\|json\\|env\\|ini\\|txt\\)\\'"It matches .yaml, .yml, .json, .env, .ini, and .txt file
extensions. Override with your own regex if your encrypted files use a
different naming scheme.
sops-before-decrypt-hook— run before each decrypt; use to setAWS_PROFILE, age key paths, etc.sops-before-encrypt-hook— run before each encrypt; same use.sops-mode-hook— standard minor-mode hook.
Example (set AWS_PROFILE based on the file’s KMS ARN):
(defun my/sops-setup-aws-profile ()
(when-let* ((file buffer-file-name)
((file-readable-p file))
(contents (with-temp-buffer
(insert-file-contents file)
(buffer-string)))
((string-match "arn:aws:kms.*?:\\([[:digit:]]+\\):" contents)))
(pcase (match-string-no-properties 1 contents)
("111111111111" (setenv "AWS_PROFILE" "dev"))
("222222222222" (setenv "AWS_PROFILE" "stage")))))
(add-hook 'sops-before-decrypt-hook #'my/sops-setup-aws-profile)
(add-hook 'sops-before-encrypt-hook #'my/sops-setup-aws-profile)For non-interactive age PIN/passphrase, set SOPS_AGE_KEY_CMD in
sops-before-decrypt-hook.
v0.2 is a clean break. If your v0.1.X config used:
:bind (("C-c C-c" . sops-save-file)
("C-c C-k" . sops-cancel)
("C-c C-d" . sops-edit-file))Remove all of the above. find-file and save-buffer now do the right
thing automatically.
If you used sops-before-encrypt-decrypt-hook, split it into
sops-before-decrypt-hook and sops-before-encrypt-hook. The
context the hooks run in also changed and affects how you write the
body:
- v0.1.X combined hook ran in two different buffers — the user’s
original buffer for decrypt (where
buffer-file-namepointed at the encrypted file) and the*sops-mode-process*-/pathtemp buffer for encrypt (wherebuffer-file-namewasniland the path lived in the internalsops--original-buffer-file-namelocal). - v0.2 has no temp buffer. Both hooks run in the same buffer that
visits the encrypted file, and
buffer-file-nameis that file’s path in both cases. Readbuffer-file-namedirectly; thesops--original-*locals are gone.
If your v0.1.X hook handled the encrypt path by reading
sops--original-buffer-file-name (or branched on whether
buffer-file-name was nil), replace it with a plain
buffer-file-name read — the encrypt-side branch is no longer needed.
See NEWS.org for the full break list.
If sops fails to decrypt a file (auth issue, missing key, etc.), the buffer
shows the encrypted contents in read-only-mode. The *sops-error: <file>*
buffer shows what went wrong. Fix the auth issue, then M-x revert-buffer
to retry.
v0.2 does not work over TRAMP — the local sops binary cannot read
remote paths. sops-mode skips remote files automatically.
Some upstream sops repos ship a .gitattributes line like
*.yaml diff=sopsdiffer plus a matching [diff "sopsdiffer"] block in
.git/config that runs sops -d on each file before diffing. That
makes git diff, git log -p, and magit-status show plaintext
for sops-encrypted YAML files. This is independent of sops-mode.
If you want plaintext diffs for your own real-secret repos, you must
opt into the textconv yourself. Be deliberate: anything that reads
the diff (the GitHub web UI if anyone copies the config there, tig,
IDE diff viewers, CI logs) will see plaintext. sops-mode protects
your buffer and on-disk file; the textconv affects diff rendering and
is a separate decision.
- future work: Interactive prompt handling for age PIN, yubikey, passphrase
GPL-3.0-or-later.