monkeys

How it works

monkeys is one binary with no daemon, no configuration file and no storage of its own. A secret is a keychain item; a command is a child process; the rest is plumbing between the two.

Where a secret is kept

On macOS each secret is a generic keychain item under the service monkeys, with the key as its account, so foo.test/DATABASE_URL is one item and security find-generic-password -s monkeys -a foo.test/DATABASE_URL finds it. Items are stored with kSecAttrAccessibleAfterFirstUnlock, which is why a command in a boot script can read one before anybody logs in.

On Linux the same two attributes go to the Secret Service over D-Bus, through secret-tool, so the secret sits in whatever keyring the desktop already runs, GNOME Keyring or KWallet.

Nothing of monkeys's own is written anywhere. There is no database to back up, and the desktop's own tools list and delete what it stores.

How a command gets one

run reads the secrets it needs, forks, and the child execvps your command with those keys in its environment. Nothing reaches the command line, so the process table and your shell history hold the key names at most, and nothing is written to a file for the command to read.

The parent stays alive rather than replacing itself, because something has to watch what comes back. It forwards the command's exit status, and when the command dies from a signal it re-raises that same signal on itself, so a Ctrl-C behaves as it would without monkeys in front. On a terminal the child is given a pty, so a command that colours its output or asks a question still sees a terminal, and window resizes are passed through.

Values from .monkeys are put in the same environment straight from the file. The vault never sees them.

How redaction works

The parent scans the child's standard output and standard error as they stream, looking for the bytes of each secret it handed over, and replaces a match with [redacted KEY]. The scan is a Boyer-Moore-Horspool search per secret, so the cost does not grow with how much the command prints.

Only what could still be the start of a secret is held back, never more, so output is not buffered waiting for a command to finish. On a terminal the held bytes are shown as * and taken back the moment they turn out to be ordinary, which keeps a prompt from looking stuck.

A file the command opens itself never passes through monkeys, so a secret that must land in a file gets there by the command writing it.

What export writes

export prints a shell line that looks the secret up rather than a line that contains it. What you paste into your startup file is a security find-generic-password on macOS, a secret-tool lookup on Linux, run by your shell every time it starts. The startup file holds a question, not an answer.

What a bundle is

pack writes one file: a header, then base64 of a 16-byte random salt followed by a ChaCha20-Poly1305 sealed box. The key comes from your passphrase through scrypt with N=2^17, r=8, p=1, which is about 128 MB of memory per attempt and is the point: a guess costs the attacker that memory too.

The cost lives in the header, so a file made today still opens after those numbers are raised, and unpack reads what the file says rather than what this version prefers.

What none of it protects against

Anything running as your user can read your vault, the same way it can read your shell startup files. monkeys moves secrets out of the repository and out of the files a tool stumbles on; it is not a sandbox. The caveats say where that line falls.

On this page