relic run, the CLI decrypts your secrets and passes them to the runner, which spawns your command in an isolated environment with those secrets as environment variables.
The runner is written in Rust (compiled as a C-compatible dynamic library) because secret injection requires low-level control over process environments, memory, and signals that JavaScript runtimes do not provide.
How It Works
1
CLI prepares the data
The CLI decrypts secrets and serializes them as JSON along with the command to run.
2
FFI call
The CLI calls the runner’s
run_with_secrets function via Bun’s dlopen FFI. Two
null-terminated C strings are passed: the command and the secrets.3
Environment setup
On Unix, the runner clears the child process environment entirely, then re-adds only essential
system variables (
PATH, HOME, USER, SHELL, TERM, LANG, LC_ALL, LC_CTYPE,
TMPDIR, TZ). Secrets are injected on top.4
Process spawn
The child process is spawned with the clean environment. The runner forwards SIGTERM and SIGINT
so your process can shut down gracefully.
5
Cleanup
After the child exits, secret values are zeroed in memory (via
Zeroizing). The TypeScript side
also zeroes its buffers.Security Measures
Platform Support
Windows support is limited. The child process inherits the full parent environment instead of
starting clean. Full Windows support with an environment allowlist is planned.
Prebuilt Binaries
The runner is compiled as a C-compatible dynamic library (cdylib):
Prebuilt binaries ship in
apps/cli/prebuilds/<platform>/. In development, the CLI loads from packages/runner/target/release/.
Building from Source
target/release/librelic_runner.dylib (macOS) or target/release/librelic_runner.so (Linux).