monkeys

run

Run a command with the secrets it needs, and nothing else.

monkeys run [@profile] <KEY[,KEY...]> <command> [argument...]
monkeys run [@profile] --all <command> [argument...]
monkeys run <command> [argument...]                    # inside a project

Runs the command with the named secrets in its environment, and nowhere else. Nothing in the line holds a secret, so nothing you write can spill one:

monkeys run OPENROUTER_API_KEY ./hello.sh
monkeys run OPENROUTER_API_KEY,GITHUB_TOKEN ./deploy

Once every secret is in place, run stays between the command and your terminal to redact what comes back; the exit status and the signals are the command's own.

Inside a project

In a directory that holds a .monkeys file, or below it within the checkout, run takes only the command and reads the file's keys from the default profile. A leading @profile picks another declared one:

monkeys run ./hello.sh
monkeys run npm run dev
monkeys run @production ./deploy

A key the file already lists is refused rather than run as a program:

monkeys run STRIPE_SECRET_KEY ./hello.sh
monkeys: ~/foo/.monkeys already lists STRIPE_SECRET_KEY for @test
inside a project, run takes only the command: monkeys run <command>

The profile's values, the KEY=value lines of the file, go into the environment as well, straight from the file:

monkeys run sh -c 'echo "$API_URL on port $PORT"'
http://localhost:8080 on port 3000

--all

--all means every key remembered under the profile, listed or not, for when you would rather not say which:

monkeys run --all ./bench

Outside a project that is every key with no profile, which is the wide end of the tool. Name the keys the command reads when you can.

When a secret is missing

A key you have not remembered a secret for stops the run before it starts, and says where it is missing from:

monkeys run OPENROUTER_API_KEY,SUPER_SECRET ./hello.sh
monkeys: SUPER_SECRET is not remembered yet
nothing happened. a human types the secret into:
  monkeys remember SUPER_SECRET
then try again.

Inside a project the message names the profile, and the remember it asks works from any directory:

monkeys: STRIPE_SECRET_KEY is not remembered yet in @foo.test
nothing happened. a human types the secret into:
  monkeys remember @foo.test STRIPE_SECRET_KEY
then try again.

That message is written to be passed on. An agent that meets it knows which keys are missing, that nothing happened, and what a human has to type.

Where the variable is expanded

run sets the variable for the command it starts, so that command is what expands it. Written into the monkeys run line itself, your own shell gets there first, and yours does not have the secret:

monkeys run OPENROUTER_API_KEY curl -H "Authorization: Bearer $OPENROUTER_API_KEY" ...
# sends: Authorization: Bearer

Single quotes pass the text through untouched, so a shell that run starts is the one that expands it:

monkeys run OPENROUTER_API_KEY sh -c 'curl -H "Authorization: Bearer $OPENROUTER_API_KEY" ...'

A script file works for the same reason, and reads better.

A variable the shell already exported is overridden for that command. Keys you leave out are passed through untouched.

What comes back

A remembered secret in the command's output comes back as [redacted KEY]:

monkeys run OPENROUTER_API_KEY sh -c 'echo "key=$OPENROUTER_API_KEY"'
key=[redacted OPENROUTER_API_KEY]

That is the reflex this exists for. An agent that meets an empty variable will echo it, and now the echo says which secret was there and nothing else. The output is streamed as it arrives. A byte is held back only while it could still be the start of a secret, and on a terminal that moment shows as * until the next byte settles it.

It catches the secret written whole or in pieces, on stdout or stderr. It does not catch the secret transformed, so echo $KEY | base64 goes through; this is for the reflex, not for someone trying.

A short secret is redacted wherever it appears, so remember secrets here and keep PORT=3000 as a value line in .monkeys, which is public and passes through untouched.

Writing a secret into a file

A file that has to end up with the secret in it takes the redirect inside the command, where the secret goes from the child straight into the file and never passes monkeys:

monkeys run OPENROUTER_API_KEY sh -c 'envsubst < template > config'
token = <the secret>

Written the way a shell reads it first, with the redirect outside, the file is monkeys's own output and the redaction lands in it:

monkeys run OPENROUTER_API_KEY envsubst < template > config
token = [redacted OPENROUTER_API_KEY]

A secret leaves through a command's own file descriptor, so which side of monkeys the redirect sits on decides what the file gets.

A shell with a profile

monkeys run @production zsh hands a whole profile to one shell, which forgets it on exit. That is the way to work with a profile for a while without putting it into every shell you open.

On this page