Put the key where every tool finds it
One key covers the whole family. manni meta encrypts a frontmatter value
whose schema marks it x-manni-encrypt. manni cite encrypts the path of a
private source. Both read the same key, and neither has a key of its own.
This page settles where that key lives before anyone marks a field or mints a citation. Two people need it. Sara owns the schema that marks a property, and Devin owns the job that has to keep passing once it does.
Every transcript below is real output from the built tool.
The two places a key can live
Section titled “The two places a key can live”| Where | How it is written | Read by |
|---|---|---|
manni.config.yaml |
encryptionKey: at the top level, beside collections: |
Every tool, on every machine that has the file |
| The environment | MANNI_ENCRYPTION_KEY |
Every tool, only where the variable is exported |
The environment wins. When MANNI_ENCRYPTION_KEY holds a value, the config’s
key is not read at all. An empty variable counts as unset, which is what a
missing CI secret expands to. The
configuration reference
has the full contract.
Both places take the same value, at least 32 hex or base64url characters.
manni key set generates 64 hex characters, which is 256 bits. A value you
pick by hand is used as key material exactly as you typed it. It is only as
strong as it is random.
Write the key to the config
Section titled “Write the key to the config”This is the short path, and it suits a private repository. manni key set
creates manni.config.yaml when there is none, and edits it in place when
there is. It never prints the key.
-
See what it would write.
--dry-runwrites nothing:Terminal window $ manni key set --dry-runmanni: manni.config.yaml is not ignored by git: once committed, anyone who can read the repository can decrypt every encrypted value. Prefer MANNI_ENCRYPTION_KEY for a shared repository.Would write encryptionKey to manni.config.yaml.# exit 0 -
Read the warning before you drop the flag. It fires when the file about to be written is not covered by
.gitignore. It is a warning rather than a refusal, because a private repository may legitimately keep its key beside its pages. -
Write it.
Terminal window $ manni key setmanni: manni.config.yaml is not ignored by git: once committed, anyone who can read the repository can decrypt every encrypted value. Prefer MANNI_ENCRYPTION_KEY for a shared repository.Encryption key written to manni.config.yaml.# exit 0
The key lands at the top level, outside every tool’s section, because every tool reads it:
encryptionKey: <64 hex characters manni key set generated>collections: - name: site paths: ["docs/**/*.md"]Or keep it in the environment
Section titled “Or keep it in the environment”A repository other people can read wants the key somewhere they cannot. The environment is that place, and it is what a CI secret expands into.
-
Generate 256 random bits, the same size
manni key setmakes:Terminal window openssl rand -hex 32 -
Store the value as a secret named
MANNI_ENCRYPTION_KEY, and export it in your own shell. The docs never show a key, so neither should a log. -
Leave
encryptionKey:out of the config.manni key setrefuses while the variable is set, because a key written to config would never be read:Terminal window $ manni key setmanni: MANNI_ENCRYPTION_KEY is set, so a key written to config would never be read. Unset it, or keep the key in the secret.# exit 2
Rotation follows the same split. A key in the config is replaced for you. A key in the environment is replaced by you, and Rotate the key without breaking CI is the runbook for both.
Who on the team needs it
Section titled “Who on the team needs it”The key is not a credential every contributor carries. Hand it out by what someone has to do, not by seniority.
| Person | Needs the key | Why |
|---|---|---|
| Sara, authoring the schema | No | A mark is a schema change. She reads the rules, not the values. |
| Maya, running the check locally | Only to verify an encrypted value | Without it, findings on encrypted values are dropped and nothing fails. |
| A contributor fixing one page | No | The values they must not read are the ones the key protects. |
| Devin, wiring the CI job | Yes, as a secret | The gate that verifies encrypted values is the one that needs to decrypt them. |
| Whoever runs a rotation | Yes | manni key rotate decrypts every value and writes it back. |
A job without the secret still runs, and the section below says what it does instead. That is what lets a public pull request job stay useful while the key stays in the private one.
What each tool does with no key
Section titled “What each tool does with no key”Nothing refuses to start. Reads degrade, and writes stop.
| Command | With no key available |
|---|---|
manni meta validate |
Findings at or under an encrypted value are dropped. One warning on stderr says how many went unverified. The exit code is unaffected. |
manni meta get, and a SELECT in manni meta query |
The ciphertext is returned, which is what they return with a key too. |
manni meta fill, a manni meta query write, manni cite add --encrypt |
On a terminal, each offers to generate a key and carries on. Off one, each refuses with exit 2. |
manni cite check |
An encrypted source is source-missing, and the run fails. --no-check-sources reports it skipped instead, and passes. |
manni key rotate |
Refused with exit 2. There is nothing to re-encrypt from. |
Validation goes quiet, not green by accident
Section titled “Validation goes quiet, not green by accident”The warning is the part to read. It counts the values nobody checked:
$ manni meta validatemanni: 3 encrypted values were not verified: no encryption key is available. Set MANNI_ENCRYPTION_KEY, or run `manni key set`.✓ docs/auth.md✓ docs/stale.md✓ docs/wrong.md
3 files checked, 3 passed, 0 failed, 0 errors# exit 0The same three pages under the key report what was wrong all along:
$ manni meta validate✓ docs/auth.md✗ docs/stale.md /owner /owner does not decrypt under the current key: encrypted under another key, or edited by hand. (owners.yaml:9) [encrypted:unreadable]✗ docs/wrong.md /owner must be equal to one of the allowed values (owners.yaml:7) [../encrypted.schema.json]
3 files checked, 1 passed, 2 failed, 2 errors# exit 1A baseline written without the key does not record the dropped findings. They come back as new on the first run that has it.
A citation without the key is a failure, unless you say otherwise
Section titled “A citation without the key is a failure, unless you say otherwise”An encrypted source path cannot be resolved without the key, so the check cannot tell a moved line from a deleted file:
$ manni cite check docs/limits.md✗ docs/limits.md ✗ fetch-timeout :15 current ~AR0x…:2 missing (no encryption key is available to decrypt it)
1 file checked, 0 passed, 1 failed, 1 finding# exit 1--no-check-sources is the flag for a job that was never meant to read the
sources. It judges the page end alone:
$ manni cite check --no-check-sources docs/limits.md✓ docs/limits.md ✓ fetch-timeout :15 current ~AR0x…:2 skipped
1 file checked, 1 passed, 0 failed, 0 findings# exit 0Run that in the public job, and the private job with the key. The two-repository layout is on Public docs, private code.
A write asks once, on a terminal
Section titled “A write asks once, on a terminal”manni meta fill, a manni meta query write, and manni cite add --encrypt
all create encrypted values. Each offers to generate a key when it is run on a
terminal, then carries on. Off a terminal, which is every CI job, each refuses:
$ manni meta query "UPDATE docs SET owner = 'billing' WHERE _path = 'docs/auth.md'"manni: /owner must be encrypted, and no encryption key is available. Run `manni key set`, or set MANNI_ENCRYPTION_KEY.# exit 2
$ manni cite add docs/auth.md:5 src/limits.ts:1 --id max-files --encrypt --dry-runmanni: src/limits.ts:1 must be encrypted, and no encryption key is available. Run `manni key set`, or set MANNI_ENCRYPTION_KEY.# exit 2manni meta validate, manni cite check, manni cite update and
manni key rotate never ask. A read that prompted would be a read that blocked
a pipeline.