Secrets
Read secrets from 1Password or age-encrypted files into generated configuration, or decrypt whole files during apply.
local rb = require("rootbeer")Providers
1Password (op)
Rootbeer installs its own 1Password CLI, op@2.39.0 from the signed package index, into /opt/rb on the first provider call. It runs that store executable directly, without using your shell's op or changing any profile, and holds it from rb gc while rb runs. Field reads, document downloads, and identity_op share it throughout planning and apply.
The CLI stays in the store until rb gc; secret values are never cached. --offline can't install it, so providers that need it fail offline. First use can download the CLI, including during a dry run that evaluates rb.secret.op(). Deferred document and age-file operations do not prepare their tools during a dry run.
Your normal 1Password account authentication still applies. Touch ID / biometric prompts surface synchronously when the CLI runs. The provider version is pinned by Rootbeer; apply --update does not change this internal dependency.
Use a secret in a config file:
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"))Save a document, such as an SSH key or certificate:
rb.secret.op_document("op://Private/work-ssh-key", "~/.ssh/work_rsa", {
mode = 0x180, -- 0o600
})The mode option sets the file permissions so only your user can read and write the key.
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.
Age
Age decryption is built in; rage is only needed to encrypt source files. Native X25519 identities (AGE-SECRET-KEY-...) are supported, including identity files with comments and multiple keys. SSH keys, plugins, and passphrase-encrypted identities or ciphertext are not supported. Binary and ASCII-armored ciphertext are accepted.
Encrypt using the public recipient (safe to commit):
rage -r age1… -o private/sessionizer.zsh.age sessionizer.zshCommit the encrypted file, keeping plaintext and private keys outside Git. For encrypted snippets, the decrypted text preserves all whitespace:
local identity = { identity = "~/.config/sops/age/keys.txt" }
local sessionizer = rb.secret.age("private/sessionizer.zsh.age", identity)For whole files, decryption happens only during apply. Writes are atomic and permissions default to 0600; existing destination symlinks are replaced.
rb.secret.age_file("private/config.age", "~/.config/example/config", {
identity = "~/.config/sops/age/keys.txt",
mode = 0x180,
})Instead of identity, use identity_op = "op://Development/AGE Key/credential" to fetch a complete native private key from 1Password. This supports first-run bootstrap without first writing a key file. Exactly one identity option is required. Fields containing only the key suffix must be updated to contain the full AGE-SECRET-KEY-... value. Identity file paths are resolved relative to the script directory, with ~ expansion.
Snippet decryption needs its key during planning: a key file scheduled with rb.file() in the same run does not yet exist. Whole-file decryption can use a key written by an earlier operation during apply. Installed plaintext remains readable locally according to its file permissions.
Previewing changes
rb.secret.op() reads a value while evaluating your configuration, including when you run rb apply --dry-run. You must be signed in to 1Password, and the value becomes part of the in-memory generated file content. The CLI preview shows paths and sizes, not file contents. rb.secret.age() has the same planning behavior and needs access to its identity even in dry runs. File bytes are omitted from plan debug output, but remain accessible to custom execution handlers. Plain Lua strings are not automatically redacted if printed, included in errors, or used in command arguments.
rb.secret.op_document() downloads the document only when applying changes. A preview shows the destination without fetching or displaying its contents. rb.secret.age_file() likewise defers decryption and key access until apply.
API Reference
rootbeer.secret.age(path, opts)
Decrypts an age file synchronously during planning, including dry runs. Supports binary or ASCII-armored ciphertext and native X25519 identities. Returns UTF-8 text unchanged, including trailing newlines. Plaintext enters Lua and any generated file content; avoid printing it or embedding it in command arguments. Plan debug output omits file bytes, but custom handlers can still inspect inline content. Use age_file for binary/deferred writes.
Parameters
pathstring~ supported.optsrootbeer.AgeOptsReturns
string — plaintextrootbeer.secret.age_file(path, dest, opts)
Decrypts and atomically writes an age file during apply. Dry runs neither read ciphertext nor fetch keys. Plaintext never enters Lua or the plan. Parent directories are created automatically. The destination is replaced (including any symlink) only after decryption succeeds, with the specified permissions. Native X25519 identities only; no passphrase, SSH or plugin keys.
Parameters
pathstring~ supported.deststring~ supported.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. Rootbeer prepares its packaged CLI on first use; authentication may prompt for Touch ID / biometrics.
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.AgeFileOpts
identitystringoptional~ and script-relative paths. Mutually exclusive with identity_op.identity_opstringoptionalop:// field containing complete native AGE-SECRET-KEY-... lines. Mutually exclusive with identity.modeintegeroptional0x180 (0600).rootbeer.AgeOpts
identitystringoptional~ and script-relative paths. Mutually exclusive with identity_op.identity_opstringoptionalop:// field containing complete native AGE-SECRET-KEY-... lines. Mutually exclusive with identity.rootbeer.SecretDocumentOpts
modeintegeroptional0x180 for 0o600). When set, a Chmod op is queued immediately after the deferred write.