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.shmonkeys 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.
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.shmonkeys run npm run devmonkeys 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 @testinside 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:
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 yetnothing happened. a human types the secret into: monkeys remember SUPER_SECRETthen 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.testnothing happened. a human types the secret into: monkeys remember @foo.test STRIPE_SECRET_KEYthen 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.
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:
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.
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.
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.