Secrets
rb.secret is the namespace for pluggable secret providers. Each provider wraps an external CLI (1Password's op, in the future rage, …) and exposes the same two-shape API: a sync read for embedding into config you generate in Lua, and a deferred write for materializing binary blobs straight to disk.
local rb = require("rootbeer")Two shapes, two use cases
Pick the shape that matches what you're doing with the secret.
┌─────────────────────────┬──────────────────────────────────────────┐
│ You want the value… │ Use… │
├─────────────────────────┼──────────────────────────────────────────┤
│ embedded in a string │ rb.secret.<provider>(reference) │
│ (config files, env vars)│ → returns string at plan time │
├─────────────────────────┼──────────────────────────────────────────┤
│ as a standalone file │ rb.secret.<provider>_document(ref, dst) │
│ (SSH keys, certs, …) │ → deferred write, bytes never in Lua │
└─────────────────────────┴──────────────────────────────────────────┘The split exists because the plan log records every operation rootbeer will perform. Inline secrets read at plan time become part of the generated file contents (which is what you want when templating a config). Binary blobs you only want to materialize on disk should never enter that log — the deferred form keeps the fetch in the apply phase, where the bytes go straight from the provider CLI to the destination file.
Plan vs apply timing
| Call | When the provider runs | Visible in rb plan output |
|---|---|---|
rb.secret.<provider>(ref) | Plan time (synchronous) | The fetched value is embedded in the resulting WriteFile content. |
rb.secret.<provider>_document(…) | Apply time (deferred) | A fetch <provider> <ref> preamble + write <dst> (deferred) line. |
A consequence worth knowing: under the sync form, rb plan must already have access to the provider (1Password unlocked, etc.) because the value is needed to construct the plan. Under the deferred form, rb plan does not touch the provider at all — useful for previewing changes in CI or on a fresh machine.
Providers
1Password (op)
Requires the 1Password CLI to be installed and signed in. Touch ID / biometric prompts surface synchronously when the CLI runs.
Embed a field into a generated config file — the dominant use case for things like API keys, tokens, and URLs that live inside dotfiles you template:
local lines = {
"[settings]",
"debug = false",
'api_url = "' .. rb.secret.op("op://Development/WakaTime/url") .. '"',
'api_key = "' .. rb.secret.op("op://Development/WakaTime/credential") .. '"',
}
rb.file("~/.wakatime.cfg", table.concat(lines, "\n"))Materialize a binary document with strict permissions — for SSH keys, GPG keys, certificates, license files, or anything you'd otherwise paste into a file. The bytes flow from op document get straight to disk:
rb.secret.op_document("op://Private/work-ssh-key", "~/.ssh/work_rsa", {
mode = 0x180, -- 0o600
})The mode option queues a chmod immediately after the write, so SSH won't reject the key on the next connection.
Reference shapes. Both functions accept op://-style references:
op://<vault>/<item>/<field>— a specific field within an item.op://<vault>/<item>— used withop_documentto fetch the document attached to an item.
Adding a new provider
The Lua surface follows the same two-shape convention so users get a predictable API across providers. At the Rust layer, deferred writes flow through a single Op::WriteFile { source: WriteSource::<Provider> } variant — there is no per-provider write op. To add a provider:
- Add a
WriteSourcevariant inplan.rscarrying whatever the provider needs to fetch at apply time (e.g.Rage { ciphertext: PathBuf, identity: PathBuf }). - Extend
resolve_sourceinapply.rswith the shell-out, andWriteSource::fetch_labelinplan.rsso the CLI announces the fetch automatically. - Add
rb.secret.<provider>(…)(sync) andrb.secret.<provider>_document(…)(deferred) bindings inlua/secret.rs, plus matching annotations inlua/rootbeer/secret.lua.
No CLI changes are required — the dry-run / apply output picks up the new provider through fetch_label.
API Reference
rootbeer.secret.op(reference)
Reads a secret from 1Password via the op CLI. Runs synchronously at plan time so the value can be embedded into strings, file contents, or other config you compose in Lua. The op CLI must be installed and authenticated (Touch ID / biometrics may prompt).
Use the sync form when you need the value in Lua (templating, config composition). For raw binary files that should never enter Lua memory, use rb.secret.op_document instead.
Parameters
referencestringop:// reference (e.g. "op://vault/item/field").Returns
string — The secret value, with any trailing newline stripped.rootbeer.secret.op_document(reference, dest, opts)
Materialises a 1Password document to disk via op document get. The fetch is deferred to the apply stage — the binary contents never enter Lua memory and the secret is never written to the plan log.
Supports ~ expansion and relative paths (anchored to the script directory). Parent directories are created automatically. When opts.mode is set, a chmod is queued immediately after the write (useful for SSH keys, GPG keys, certificates, etc. that require restricted permissions).
Parameters
referencestringop:// reference to the document (e.g. "op://Private/work-ssh-key").deststring~ expansion supported).rootbeer.SecretDocumentOpts
modeintegeroptional0x180 for 0o600). When set, a Chmod op is queued immediately after the deferred write.