Getting started

From "I just installed secreq" to "my first wrap is running with a secret from my store."

Before you start

  • A secret store with a CLI on your $PATH: built-ins exist for 1Password (op), macOS Keychain, LastPass and pass. You can declare your own; see providers.
  • 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:

secreq wrap gh \
  --env GITHUB_TOKEN=secret://op/Personal/GitHub/credential \
  --reason "GitHub API access"

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 shadowed
which 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 @you you/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.

Next