A command line secrets manager for Linux and macOS. Secrets are organised into
encrypted vaults: one file each, edited in $VISUAL or $EDITOR, searched
with regular expressions.
Archives are published on the
releases page: one per tagged
version, plus a dev release rebuilt on every push to main.
| Platform | Asset |
|---|---|
| Linux x86_64 | mrs_linux_x86_64.tar.gz |
| Linux arm64 | mrs_linux_arm64.tar.gz |
| macOS Apple Silicon | mrs_darwin_arm64.tar.gz |
The archive carries LICENSE and README.md alongside the binary, so name the
binary rather than extracting everything into the current directory:
tar -xzf mrs_linux_x86_64.tar.gz mrs
sudo install -m 755 mrs /usr/local/bin/mrsTo compile from source, with Go and Make:
git clone https://github.com/andornaut/mrs.git
cd mrs
make install- A vault holds secrets separated by blank lines. A line of nothing but spaces or tabs separates them too, and is written back as a blank one.
- The first line of a secret is its key; the rest is its value.
- Every other line is kept as typed: indentation, trailing spaces, and lines
that begin with a
#. - Secrets are sorted by key, ignoring case, when saved. Two may share a key, and
mrswarns when they do, on import as well as on save. - A file given to
vault add --import-fileis stored as it is written, so it keeps its own order until the vault is next saved. mrs addandmrs editopen$VISUALor$EDITORon the secrets alone, and encrypt whatever the editor saves.mrs add --helpstates the format.
Commands that read or write the secrets in a vault name it with --vault,
which accepts a prefix, or with --file, which names a vault file wherever it
is kept. Commands that create, rename or destroy a vault take its whole name as
an argument.
| Command | Does |
|---|---|
mrs add |
Add secrets in an editor |
mrs edit |
Edit secrets in an editor |
mrs search <regular expression>... |
Print matching secrets |
mrs export |
Print every secret |
mrs vault ls |
Print vault names |
mrs vault default |
Print the default vault |
mrs vault add <name> |
Add a vault |
mrs vault change-password <name> |
Re-encrypt under a new password |
mrs vault rename <source-name> <target-name> |
Rename a vault |
mrs vault rm <name> |
Delete a vault, after confirming |
search matches keys only, unless --full. Matching is case insensitive, and
arguments are joined, so mrs search bank account matches bank account.
vault ls prints names sorted ignoring case, as secrets are sorted by key.
mrs --version prints the version, and -h, --help works on every command.
| Flag | Commands | Supplies |
|---|---|---|
-v, --vault |
add, edit, search, export |
the vault's name, or the start of it |
--file |
add, edit, search, export |
the path of a vault file, instead of a name |
-p, --password-file |
add, edit, search, export, vault add, vault change-password |
a file holding the vault's current password |
-n, --new-password-file |
vault change-password |
a file holding the password to change it to |
-i, --import-file |
vault add |
a file of unencrypted secrets to seed the vault with |
-f, --full |
search |
match values as well as keys |
-y, --yes |
edit, vault rm |
the answer to the confirmation |
--force |
add, edit, vault add, vault change-password, vault rm, vault rename |
permission to repair a lock file that cannot be used |
--path |
vault ls, vault default |
paths instead of names |
A short flag means the same thing on every command. --file, --force and
--path have no short form, because -p is the password file wherever there is
one, -f is --full on search, and --force is worth spelling out. --path
takes no value: it asks vault ls and vault default to print paths rather
than names.
--force repairs a lock file that cannot be opened, because its mode forbids
it, a directory sits in its place, or it is a symlink that does not resolve. It
never takes a lock another process holds: taking one would mean deleting the
lock file, leaving the two processes holding two different files. A held lock is
refused with or without the flag:
$ mrs vault rm work --force --yes
Error: vault work is currently locked by another process. --force repairs a lock
file that cannot be used, and does not take a lock another process holdsWait for the other process, or stop it. A lock file left behind by a process that died needs neither: it is already re-lockable.
add, edit, search and export name a vault with -v, which takes a
prefix. An exact name always wins, whatever longer names begin with it: with
work and work-archive, -v work is work. Short of one, a prefix has to
fit exactly one vault:
$ mrs edit -v alph
Error: "alph" begins the name of 2 vaults: alpha, alphabet. Use the whole name of the one you mean
Without -v, those four use $MRS_DEFAULT_VAULT_NAME, or the only vault if
there is just one. Unlike -v, the configured name has to match exactly. No
vaults, or several with nothing configured, is an error rather than a guess.
vault add, vault change-password, vault rename and vault rm name
the vault as an argument instead, and take no prefix at all: each one creates,
re-keys, moves or destroys a vault, so a name short of the whole thing must not
reach a neighbouring one. They name the closest vault when given a prefix:
$ mrs vault rm alph
Error: vault "alph" not found. Did you mean "alpha"?
Names may hold ASCII letters, digits, _ and -, up to 200 characters.
add, edit, search and export also take --file, which names a vault
file directly: one on removable media, or in a directory that is synced
elsewhere. Nothing is looked up, so it names no prefix and falls back to no
default, and it is refused alongside -v:
$ mrs search --file /mnt/usb/work.<salt> aws
Vault password:
1 secret matched "aws" in vault /mnt/usb/work.<salt>The file still has to be named <name>.<salt>, because the key is derived from
the salt the filename carries and there is nowhere else to read it from. Copy or
move a vault with its salt intact and it opens with the password it always had.
Its lock file and the temporary file of a save are its siblings, so
mrs writes in the directory the vault is in and needs to be able to write
there. A vault outside the vault directory is not listed by vault ls, and
$MRS_DEFAULT_VAULT_NAME cannot name one.
- Prompted on the terminal with echo off. At least 8 characters, and no newline. A password mrs will not accept is refused before it asks you to confirm it.
- Without a terminal there is nothing to prompt from, so pass
--password-file. A trailing newline is trimmed, soecho 'pw' > pwworks; other whitespace is part of the password. - A save replaces the vault and writes no copy of it. Nothing beside a vault goes on opening with a password it no longer has, and nothing is a way back from an edit: keep your own copy if you want one.
mrs edit that would empty a vault, and mrs vault rm, ask first.
-y, --yes answers in advance. Without a terminal and without --yes, mrs
fails rather than assume an answer:
$ mrs vault rm old < /dev/null
Error: cannot ask "Delete vault old?": stdin is not a terminal. Use --yes to answer it
stdout carries what a caller consumes: vault names from vault ls and
vault default, secrets from export and search. Warnings, errors and
reports go to stderr, so mrs export > secrets and mrs search key | less
carry the secrets alone. Prompts go to the terminal itself, so that redirecting
stderr does not leave mrs waiting for an answer nobody can see.
$ mrs search bar
Vault password:
1 secret matched "bar" in vault example
another secret key bar
bank account number: 1234
| Code | Meaning |
|---|---|
| 0 | it worked |
| 1 | it failed |
| 2 | it was typed wrong: no command, an unknown command or flag, or a missing or extra argument |
| 3 | mrs search ran and matched nothing |
| 128+n | a signal ended it: 129 SIGHUP, 130 SIGINT, 131 SIGQUIT, 143 SIGTERM |
A wrong invocation prints the usage that would have been right; a command that
ran and failed does not. mrs --help writes help to stdout and reports success.
| Path | Holds |
|---|---|
$MRS_HOME/vaults/<name>.<salt> |
the vault, mode 0600 |
$MRS_HOME/vaults/<name>.lock |
the lock on the name, empty |
$MRS_TEMP/mrs/<run>/ |
decrypted secrets while an editor is open, mode 0700 |
- The vault directory is mode 0700.
mrsnarrows permissions it finds wider than that and never widens them. - One process at a time may write a vault or claim its name. Reads take no lock, and every write is atomic, so a reader never sees a half-written vault.
- A lock file outlives the vault it is named for, because removing it would leave two processes holding two different files.
- The temporary directory is created only when secrets are decrypted, and
removed when
mrsexits, including on SIGHUP, SIGINT, SIGQUIT and SIGTERM. A SIGKILL or a power loss leaves the decrypted file behind, because nothing runs to remove it and no later run sweeps it up; delete it by hand.
A file in the vault directory that is not shaped like a vault is named on stderr and otherwise left alone. A vault that mrs cannot read is a different thing: a symlink whose target is not there, a directory where a file should be, or a vault written by a release that derived its key differently. Those are listed with a warning saying why, keep their names, and can be renamed or deleted like any other; only the commands that have to read them fail.
| Environment variable | Description |
|---|---|
EDITOR |
The editor add and edit open, if $VISUAL is unset (default: the first of vim, vi, nano that is on PATH). May carry arguments, such as vim -n. Quote a path that contains spaces. |
MRS_DEFAULT_VAULT_NAME |
The vault to use when --vault is not given. Must name one exactly (default: the only vault, if there is just one). |
MRS_HOME |
Where vaults are stored (default: $XDG_DATA_HOME/mrs, else $HOME/.local/share/mrs). |
MRS_TEMP |
Where decrypted secrets are written while an editor is open (default: $XDG_RUNTIME_DIR, else the system temporary directory). |
VISUAL |
The editor add and edit open, in preference to $EDITOR. Same form. |
- 256-bit AES-GCM.
- PBKDF2-SHA256, 600,000 iterations, over a 32 character salt that is unique per vault and carried in its filename.
- Vaults written with the earlier 4,096 iterations are not read.
mrssays so when it meets one, rather than reporting the wrong password it cannot tell it from. Every release up to v0.1.7 reads one and re-encrypts it at 600,000 iterations on the next save, so open and save such a vault with one of those.
The AES-GCM seal and open in internal/crypto
are copied from cryptopasta, which its
author placed in the public domain under CC0 to be copied rather than imported.
The ciphertext is nonce|ciphertext|tag with a random 96-bit nonce per save.
See the Makefile. make test runs both layers:
internal/holds unit tests, beside the code they exercise.test/e2edrives a compiledmrsagainst real vault files, a real editor process and real encryption. Nothing is mocked. Each test gets its own vault, temporary and home directories, so they run in parallel; the fake editor is scripted through the environment, and the cases that answer a prompt runmrson a pseudo-terminal, because without one it reports that it cannot ask.
To release, push a semantic version tag from main:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0The test workflow runs the tests and
golangci-lint on every branch and pull request. The
release workflow calls it, then builds and
publishes the binaries with GoReleaser. Every push to
main republishes the rolling dev release the same way.