Skip to content
OzhiakiPublic

About

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

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

18 Commits

Folders and files

Repository files navigation

fenn

CI crates.io

Run a command with its secrets fetched just before they're needed, passed only to that command, and gone when it exits. Your pointer file holds references, never values.

fenn demo — a wrapped command sees the token; the same shell never does

The directory hook wraps only the commands you declare, so the secret enters that child and nothing else — release above sees NPM_TOKEN, printenv in the same shell sees nothing.

$ 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.

What fenn keeps out of reach

  • Never on argv — values go into the child's environment, never a command line, so they can't be read from ps//proc.
  • Never on disk — env injection needs no temp files.
  • Never in your shell — secrets land in the child only; your interactive shell and this process's own environment stay clean.
  • Can't leak by accident — the Secret type prints <redacted> for Debug/Display and zeroizes its buffer on drop.
  • Fail closed — if any reference fails to resolve, fenn runs nothing.

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 — automatic and scoped

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 gives you both properties at once: 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.

fenn vs direnv, briefly — direnv is a general per-directory environment manager; fenn is direnv for secrets, not a replacement:

  • Scoped, not ambient — the secret enters the wrapped child, not your interactive shell and everything it spawns.
  • Data, not a script — a pointer file is flat KEY=cmd://… lines you can read before trusting; direnv runs .envrc as shell on every cd. fenn runs no resolver until you invoke a wrapped command.
  • No stray copies — direnv also keeps a reversible snapshot of the loaded values in an exported DIRENV_DIFF (verified plaintext-recoverable on direnv 2.37.1), so a secret ends up in your environment twice; fenn puts nothing in your shell at all.

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.

About

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

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages