monkeys

LLMs read .env; not anymore.

For LLMs, cat .env is just too tempting, and once it's in the transcript, it's there for good.

Explored 4 files
Thought for 42s
Opened .env
Reading .env

The usual ways to live with that:

  1. Ignore it.
  2. Trust the provider.
  3. Rotate the key after it leaks.

Someone will call it a skill issue. It isn't. There has never been a safe way for the people on a project to share a secret and use it, so it went in a file. C had a memory problem too, and being careful didn't fix it. Rust did.

monkeys keeps each secret in your vault and hands it to one command at a time. Nothing prints a stored secret, the command you hand it to included, so there is nothing to read.

Install

One binary, no runtime. The script picks the build for your machine, checks the published checksum, and puts it in ~/.local/bin; Homebrew upgrades it along with everything else.

For a coding agent, install the skill as well. It is what makes the agent reach for monkeys on its own instead of asking you to paste a secret; a key it needs but you have not stored comes back as a message that says what to ask you for.

The plugin follows the Agent Plugins layout, and the third tab is the one file any other agent needs, in whatever directory it reads skills from.

monkeys run refusing a missing secret, then running the command, then preview

Store a secret once

Type sesame at the prompt. It goes into your vault, the keychain on macOS and the Secret Service on Linux, and nothing you type lands in your shell history.

Spend it on one command

Name the keys the command reads, then the command, written the way you always write it. run puts the secret in that one process and becomes it.

The right word reached the command, and what the command printed came back with the secret taken out. The single quotes matter: the shell run starts is the one that has the secret, so it has to be the one that expands $SUPER_SECRET.

Without monkeys the door stays shut:

Take the word away and try the door again:

Nothing ran at all. A missing secret stops run before the command starts, and the message says what to do, which is what an agent passes on.

Let the project list the keys it needs

A .monkeys file next to the code names its namespace on the first line and lists the keys under a profile. Commit it. In that directory, run takes only the command, and the script reads the variable the way any program does. A value that was never secret, PORT=3000 and the like, sits in it as KEY=value.

.monkeys
hello.sh

A profile line can name several profiles, and a file can hold several blocks. A profile's keys are those of every block that lists it; the first profile in the file is the one run uses when none is given, and one the file does not declare is refused. A prefix that fits only one declared profile is enough, the way a short git hash is.

.monkeys

A secret missing in one profile stops that profile alone, and only when it is used. monkeys fill @production --with @test fills the second profile with what the first has and it lacks, and doctor reads the whole file:

Hand the profile to a teammate

pack asks for a passphrase and writes /tmp/a.monsecrets, outside the repository: every profile the file declares, or the ones --only @test names, with their keys and secrets, sealed. Send the file however you like, and the passphrase another way.

On the other machine, unpack asks for the passphrase, stores the secrets, writes .monsecrets at the root of the checkout, wherever inside it you run it, and deletes the bundle. That is the only way a secret leaves the vault.