A login session for that store (op signin, an unlocked keychain, a
gpg-agent for pass). secreq never signs into your store for you.
A graphical session, on Linux/BSD: the consent prompt is a native
window, so it needs $DISPLAY or $WAYLAND_DISPLAY. macOS always has
one. Headless machines use --sq-yes; see
platform-support.
1. Install
curl -fsSL https://craigory.dev/secreq/install.sh | sh
Homebrew, cargo install, and verified release tarballs all work too.
install covers every channel, and how to check a download's
signature. Confirm with secreq --version.
None of them create any wraps or shims. That's the next step.
2. First-time setup
$ secreq init
┌ secreq init — first-time setup│◆ Where should secreq drop PATH shims?│~/.secreq/shims└
┌ secreq init — first-time setup│◇ Where should secreq drop PATH shims?│~/.secreq/shims│◇ Add secreq to your PATH ──────────────────────────────────────────────╮│││~/.secreq/shims isn't on PATH. I can append this to ~/.zshrc (Zsh):││││# >>> secreq managed PATH (do not edit by hand) >>>││export PATH="$HOME/.secreq/shims:$PATH"││# <<< secreq managed PATH <<<│├────────────────────────────────────────────────────────────────────────╯│▲ zsh note: writing to .zshrc (which runs after .zprofile, where homebrew│ lives) so our prepend wins on PATH. Non-interactive zsh launched externally│ (ssh non-login, cron) doesn't read .zshrc — children of your interactive│ shell inherit PATH from it though, so npm postinstalls and the like still│ see the shim.│◆ Append it?│● Yes / ○No└
┌ secreq init — first-time setup│◇ Where should secreq drop PATH shims?│~/.secreq/shims│◇ Add secreq to your PATH ──────────────────────────────────────────────╮│││~/.secreq/shims isn't on PATH. I can append this to ~/.zshrc (Zsh):││││# >>> secreq managed PATH (do not edit by hand) >>>││export PATH="$HOME/.secreq/shims:$PATH"││# <<< secreq managed PATH <<<│├────────────────────────────────────────────────────────────────────────╯│▲ zsh note: writing to .zshrc (which runs after .zprofile, where homebrew│ lives) so our prepend wins on PATH. Non-interactive zsh launched externally│ (ssh non-login, cron) doesn't read .zshrc — children of your interactive│ shell inherit PATH from it though, so npm postinstalls and the like still│ see the shim.│◇ Append it?│Yes│◆ wrote ~/.zshrc. Open a new terminal (or `source ~/.zshrc`) to pick it up.│◆ Also set up secreq as your SSH agent?│● Yes / ○No└
┌ secreq init — first-time setup│◇ Where should secreq drop PATH shims?│~/.secreq/shims│◇ Add secreq to your PATH ──────────────────────────────────────────────╮│││~/.secreq/shims isn't on PATH. I can append this to ~/.zshrc (Zsh):││││# >>> secreq managed PATH (do not edit by hand) >>>││export PATH="$HOME/.secreq/shims:$PATH"││# <<< secreq managed PATH <<<│├────────────────────────────────────────────────────────────────────────╯│▲ zsh note: writing to .zshrc (which runs after .zprofile, where homebrew│ lives) so our prepend wins on PATH. Non-interactive zsh launched externally│ (ssh non-login, cron) doesn't read .zshrc — children of your interactive│ shell inherit PATH from it though, so npm postinstalls and the like still│ see the shim.│◇ Append it?│Yes│◆ wrote ~/.zshrc. Open a new terminal (or `source ~/.zshrc`) to pick it up.│◇ Also set up secreq as your SSH agent?│No│└ Wrote ~/.secreq/config.toml. Next: `secreq wrap <binary>`, e.g. `secreqwrap gh`.
First-time setup. Installing secreq changes nothing on your PATH — init shows you the exact block it wants to add and the exact file it would go in, then waits for an answer.
init picks a shim directory (~/.secreq/shims by default, a dedicated
one, so it can't collide with asdf, pip user-installs, or anything else),
checks whether it's on $PATH, and offers to append a
sentinel-bracketed export PATH=… block to the right shell file. The block
is shown to you in full and gated by a y/N prompt; nothing touches your
dotfiles unconfirmed. Re-running is a no-op.
Restart your shell so the new $PATH takes effect, then confirm:
echo $PATH | tr ':' '\n' | grep secreq # should print your shim dir
3. Wrap your first binary
Pick a CLI you regularly hand a credential to by env var: gh, aws,
kubectl, psql, terraform. Run wrap with just the binary name and it
asks for the rest:
$ secreq wrap gh
┌ Wrap `gh`│◆ What should this wrap do?│● Inject secrets (resolve secret:// references into env vars)│○Gate only (no secrets)└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◆ Provider for the next env var│● keychain (supports store)│○lastpass│○op│○pass└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◆ Provider for the next env var│○keychain│● lastpass (retrieve-only)│○op│○pass└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◆ Provider for the next env var│○keychain│○lastpass│● op (retrieve-only)│○pass└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◇ Provider for the next env var│op│◆ Environment variable name│e.g. GITHUB_TOKEN└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◇ Provider for the next env var│op│◇ Environment variable name│GITHUB_TOKEN│◆ Locator│provider-specific address (e.g. Personal/GitHub Token/credential)└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◇ Provider for the next env var│op│◇ Environment variable name│GITHUB_TOKEN│◇ Locator│op://Personal/GitHub/credential│● Reading that as locator `Personal/GitHub/credential`│◇ Locator resolves ✓│◆ Add another env var?│○Yes / ● No└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◇ Provider for the next env var│op│◇ Environment variable name│GITHUB_TOKEN│◇ Locator│op://Personal/GitHub/credential│● Reading that as locator `Personal/GitHub/credential`│◇ Locator resolves ✓│◆ Add another env var?│○Yes / ● No└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◇ Provider for the next env var│op│◇ Environment variable name│GITHUB_TOKEN│◇ Locator│op://Personal/GitHub/credential│● Reading that as locator `Personal/GitHub/credential`│◇ Locator resolves ✓│◇ Add another env var?│No│◆ Reason (shown in consent prompt)│(empty to skip)└
┌ Wrap `gh`│◇ What should this wrap do?│Inject secrets│◇ Provider for the next env var│op│◇ Environment variable name│GITHUB_TOKEN│◇ Locator│op://Personal/GitHub/credential│● Reading that as locator `Personal/GitHub/credential`│◇ Locator resolves ✓│◇ Add another env var?│No│◇ Reason (shown in consent prompt)│GitHub API access│└ Wrapped `gh`. config: ~/.secreq/config.toml shim: ~/.secreq/shims/gh
Authoring a wrap by answering questions. Every value is checked as you go — the locator is resolved against your store before the wrap is written, so a typo fails here rather than the first time you run gh.
Prefer this path. The interactive flow checks its work: you pick the
provider from a list rather than spelling it, and the locator is resolved
against your store before the wrap is written. A bad path fails while you're still looking at
it, instead of the first time you run gh.
Everything it asks can be supplied up front, which is what you want in a
dotfiles script:
Either way you get an entry in ~/.secreq/config.toml and a five-line shim
at <shim_dir>/gh. Confirm:
secreq doctor # config valid, providers on PATH, no shim shadowedwhich gh # should point at <shim_dir>/gh
secret://op/Personal/GitHub/credential is a reference: op is the
provider, the rest is the locator, and the provider knows how to turn one
into a value. Values never appear in your config; only references do.
Wraps that inject nothing
Not every wrap carries a secret. A gate-only wrap injects nothing and
exists purely to put the consent prompt in front of a command that already
holds its own credentials. op itself is the obvious case:
$ secreq wrap op
┌ Wrap `op`│◆ What should this wrap do?│● Inject secrets (resolve secret:// references into env vars)│○Gate only (no secrets)└
┌ Wrap `op`│◆ What should this wrap do?│○Inject secrets│● Gate only (no secrets) (just require consent before the command runs)└
┌ Wrap `op`│◇ What should this wrap do?│Gate only (no secrets)│◆ Reason (shown in consent prompt)│(empty to skip)└
┌ Wrap `op`│◇ What should this wrap do?│Gate only (no secrets)│◇ Reason (shown in consent prompt)│gate the 1Password CLI itself│└ Wrapped `op` (gate-only — consent required, nothing injected). config: ~/.secreq/config.toml shim: ~/.secreq/shims/op
A gate-only wrap injects nothing — it just puts the consent prompt in front of a command. Use it for tools that already hold their own credentials.
4. Run it
$ gh repo list
⠋ secreq: waiting for approval of `gh` — approve in the popup window
Showing 3 of 3 repositories in @youyou/secreq consent for secrets on your machine public 1hyou/dotfiles shell, editor, and all the rest private 3dyou/notes a wiki that is really just files private 2w
The day after setup. gh is a shim, so the command stops and waits while secreq asks you — and the moment you approve, the real gh runs with the token already in its environment. Nothing was exported, and nothing stayed behind.
The first time, that is: your shell finds the shim, the shim execs
secreq x gh repo list, secreq auto-spawns the consent daemon, and the
command stops, which is what the wait indicator means, while the daemon
puts up a window showing what's about to happen. You press Approve, the daemon
resolves the secret and hands it back, and the real gh runs with
GITHUB_TOKEN in its environment. Anything matching that value is redacted
from its output.
Run it again from the same shell and nothing prompts: approving also
remembers. The grant belongs to that shell: a different terminal, an
editor, or an npm postinstall each get asked in their own right. See
how approval is scoped.
5. A few defaults
Unwrapped binaries pass straight through. Calling one that has no
wrap entry just execs the real thing. You can put the shim dir on PATH
before you've wrapped everything.
secreq view opens the manager window, holding your rules and your
audit log. The audit log records names, never values.
--sq-yes skips the daemon entirely and is the supported path for
CI: secreq x --sq-yes gh repo list.
--sq-raw disables masking when you actually want the value on
stdout, e.g. secreq x --sq-raw gh auth token | pbcopy.
SSH keys, too
secreq can act as your SSH agent, gating each key signature on the
same ceremony, so git push prompts you with the caller chain before
signing. See ssh-agent, including the key-custody
tradeoff against 1Password's sealed agent.