The first line names the profile; the rest are names, and # starts a
comment. Commit it. It is the secret half of .env.example,
and a project keeps one or the other, since two lists of the same names drift.
In that directory or any below it within the checkout, run takes only the
command:
monkeys run ./hello.shmonkeys run npm run dev
The profile scopes every name. monkeys set STRIPE_SECRET_KEY there stores
foo/STRIPE_SECRET_KEY, which is what run reads, and preview with no
names gives the file's names from that profile. list stays global and
shows the prefixes, so you can see which project each value belongs to.
The profile's name is the project's and its values are yours. Everyone who
clones the repository gets the same names, and each of them fills their own
keyring, so a .monkeys file can be committed and a keyring never has to be.
A profile never reads from the personal one. Names stored without a profile
are yours alone, and a name missing in @foo is missing there even when a
bare copy exists, so a project cannot quietly pick up a value meant for
another.
A profile's names are those of every block that lists it, so both profiles
here need DATABASE_URL and STRIPE_SECRET_KEY, and foo also needs
SENTRY_DSN. The first profile in the file is the one run uses when none is
given. A leading @profile picks another declared one, and a prefix that fits
only one of them is enough, the way a short git hash is. A profile the file
does not declare is refused with the declared ones listed, and a prefix that
fits several is refused with those:
monkeys run @foo ./deploymonkeys set @foo SENTRY_DSN
A value missing in one profile stops only that profile, and only when it is
used: foo can be half filled while test.foo runs. doctor reads the whole
file and shows every profile with what it has and lacks, in colour on a
terminal, and exits non-zero while anything is missing:
doctor --short says only what is wrong, one line per profile with a
problem, and nothing at all when there is none, which is the form to hand a
script or an agent:
monkeys doctor --short
missing @foo: STRIPE_SECRET_KEY,SENTRY_DSN
Two profiles of one project usually share most of their values, and the
second is filled from the first:
monkeys fill @foo --with @test.foo
filled @foo from @test.foo: STRIPE_SECRET_KEYkept 1 @foo already hadstill missing in @foo: SENTRY_DSN
fill moves only the names the target lacks and never touches a value it
already holds, so it is safe to run twice. It names every value it moved,
since a production profile filled from test is a decision to see written
down, and it exits non-zero while anything is still missing. No value is
printed. Inside a project the names are the ones the file lists for the
target; elsewhere they are whatever the source holds.
Profile names take letters, digits, _, - and .. A dotted name in the
style of a bundle identifier keeps two projects' test apart in one keyring.
Two parts, test.foo, is enough for most; a third, test.foo.lee,
is for a keyring that holds many projects and collides at two. A single word
does for a profile nothing else will collide with.
A name stored outside any project has no prefix and needs no @. That is
where a value that belongs to you rather than to a project lives, such as
the key a tool you start from anywhere reads:
monkeys set TYPESAFE_API_KEYmonkeys run TYPESAFE_API_KEY claude
Inside a project every command is scoped to that project's profile, so
those personal names are out of reach there: monkeys run reads foo/,
and monkeys set TYPESAFE_API_KEY would write foo/TYPESAFE_API_KEY. A
bare @ means the personal profile. It sets the project file aside, so
names are given again, and reaches the unprefixed names without leaving
the directory:
monkeys run @ TYPESAFE_API_KEY claudemonkeys set @ TYPESAFE_API_KEYmonkeys preview @ TYPESAFE_API_KEY
It is the same value either way; the @ only says which profile to look
in when a file would otherwise decide.
A missing value says where it is missing from, and the set it asks for works
from any directory:
monkeys run ./hello.sh
monkeys: STRIPE_SECRET_KEY is not stored yet in @foonothing ran. a human has to store it, then try again: monkeys set @foo STRIPE_SECRET_KEY
Inside a project, everything after run is the command, and a name 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 @fooinside a project, run takes only the command: monkeys run <command>
--all there means every name stored under the profile, listed or not.
--only says which, and reads the way the file is written: a @profile
line opens a block, @a,b opens one for several profiles at once, and the
names after it belong to every profile in that block. A block with no names
after it goes whole; names before any @ come from the default profile, the
first the file mentions. That is how a teammate gets test and never
production, or one key on its own:
The bundle keeps that shape, block for block, and unpack writes it back as
the project file. A leading @profile before --only means that one profile,
with the names that follow. The file name goes before --only, which takes
the rest of the line. A name a profile does not list is refused rather than
left out.
The file takes the first profile's name unless a word after pack names it. It carries each profile's name, the names the project lists for
it, and their values, sealed with ChaCha20-Poly1305 under a key scrypt
derives from the passphrase. The file is safe to send over whatever you
already use; the passphrase goes another way. A pack with a value still
missing refuses, since a bundle that fills half a profile is a bug for
whoever receives it.
The other side runs unpack anywhere inside the checkout:
The values go into that person's keyring under each bundle's profile, and
the profiles and names become a .monkeys file at the root of the git
checkout, the way .gitignore sits at the root, so monkeys run ./hello.sh works
from any directory in it. Outside a checkout the file goes in the current
directory, and a second argument names the directory outright. When a
.monkeys file is already there, unpack adds a block at the end for the
names the file does not yet list, grouped the way the bundle groups them, and
leaves the rest of the file alone.
run looks for the file from the current directory upward, nearest first, and
stops at the root of the git checkout, so a file above the checkout is never
read. Outside a checkout only the current directory counts, and where no file
is found run takes the names on the line, as it does anywhere else.
Both commands read the passphrase from standard input when it is not a
terminal, for the rare script that needs to.
A bundle has no place in a repository, and the ignore rule needs two lines,
because *.monkeys alone also matches the .monkeys file you do commit: