← Repositories

fenn

Run a command with secrets fetched just-in-time, injected leak-tight, and gone on exit — plus a safe, scoped direnv. Rust.

Language RustStars ★ 0Updated 7/22/2026View on GitHubHomepage

1passwordclideveloper-toolsdirenvdotenvenvironment-variablespassrustsecret-injectionsecretssecrets-managementsecuritysopszeroize

fenn

CI crates.io

Run a command with secrets fetched just-in-time, injected leak-tight, and gone on exit. Your pointer file holds references, never values.

$ cat .fenn.env
GITHUB_TOKEN=cmd://gh auth token      # run any CLI, capture its stdout as the secret
DATABASE_URL=env://DB_URL             # or pull from an existing env var
LOG_LEVEL=debug                       # no scheme -> a literal, passed through

$ fenn exec -- printenv GITHUB_TOKEN
gho_xxxx…                             # the child sees it…
$ printenv GITHUB_TOKEN
                                      # …your shell never does

Why

A .env file with real secrets sits in plaintext on disk, one git add . from a leak. fenn keeps only pointers on disk and fetches the real values just before they're needed, into one child process's environment, gone when it exits.

Hygiene guarantees (the point of the tool)

Honest limit: a running process holds secrets in memory by nature. fenn shrinks the exposure window and defeats accidental leaks; it does not defeat a live-memory dump.

Backends

cmd://<command> is the primitive — shell out to any backend's own CLI. Native schemes are thin rewrites onto it (see src/resolver/rewrite.rs):

op://Prod/stripe/key   ->  op read op://Prod/stripe/key
pass://prod/db         ->  pass show prod/db
sops://file.yaml#a.b   ->  sops --decrypt --extract '["a"]["b"]' file.yaml
sops://file.yaml#s[0].h -> sops --decrypt --extract '["s"][0]["h"]' file.yaml  # array index
sops://file.yaml       ->  sops --decrypt file.yaml           # whole decrypted file

env://VAR reads an existing environment variable.

cmd:// execs an argv directly — no shell, so no pipes or redirection. When you genuinely need them, sh://<line> runs the line through sh -c (opt-in, weaker posture — every shell metacharacter is live, so keep it to trusted config):

sh://vault read -field=token secret/ci | tr -d '\n'

Directory hook — a safe direnv

direnv auto-loads a folder's vars, but into your interactive shell — ambient and leaky (they sit in /proc/self/environ, visible to every process you launch). fenn fills the quadrant nobody occupies: automatic and scoped. In a trusted folder the hook wraps declared commands so they run under fenn exec — the secret reaches only that child, never your shell.

$ cat .fenn.env
# fenn: wrap npm node        # commands to auto-wrap in this folder
NPM_TOKEN=cmd://gh auth token

$ eval "$(fenn hook zsh)"    # once, in your ~/.zshrc  (or: fenn hook bash)
$ cd myproject
fenn: .fenn.env present but not allowed here — run `fenn allow`
$ fenn allow                 # trust it — pins the file's hash
$ npm publish                # runs as `fenn exec -- npm publish`; NPM_TOKEN in npm only
$ printenv NPM_TOKEN         # your shell still sees nothing

Trust is content-hashed: edit .fenn.env and the hook goes cold until you fenn allow again, so a pointer file in a freshly cloned repo does nothing on its own. fenn deny revokes.

Try it in a throwaway sandbox (fake secret, no real vaults): scripts/demo.sh.

Commands

fenn init  [-f <file>] [bash|zsh]                        # scaffold a .fenn.env + print the hook line
fenn exec [-f <file>] [--no-inherit] -- <cmd> [args…]   # resolve + run
fenn check [-f <file>]                                    # resolve, report ok/fail by name (no values)
fenn list  [-f <file>]                                    # show names + reference URIs (no values)
fenn allow [dir] / deny [dir]                             # trust / untrust a directory for the hook
fenn hook  <bash|zsh>                                     # print the shell integration for your rc

Status

Resolvers: cmd://, sh://, env://, op://, sops://, pass://. Directory hook with content-hash trust (bash/zsh). SIGTERM/SIGHUP forwarded to the child; resolver buffers zeroized.

Apache-2.0.