Authoring config.toml

You may not need this page. secreq wrap <binary> asks for everything a wrap needs and writes the entry for you. Unlike hand-editing, it resolves the locator against your store before saving, so a typo fails while you're still looking at it. Read this when you want to know what a field means, hand-edit something the prompts don't cover, or check a config into a dotfiles repo.

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

The config lives at ~/.secreq/config.toml (or $SECREQ_HOME/config.toml). It's TOML. The #:schema line points your editor at the published schema for completion and inline validation:

#:schema ./wraps.schema.json
shim_dir = "~/.secreq/shims"   # set by `secreq init`

[secrets.GITHUB_TOKEN]
ref = "secret://op/Personal/GitHub Token/credential"

[wraps.gh]
reason = "GitHub API access"
env_secrets = ["GITHUB_TOKEN"]

[wraps.aws]
reason = "AWS deployments"
env.AWS_ACCESS_KEY_ID = "secret://op/Work/AWS/access_key_id"
env.AWS_SECRET_ACCESS_KEY = "secret://op/Work/AWS/secret_access_key"

[wraps.kubectl]
env.KUBECONFIG = "secret://keychain/work/kubeconfig"

Each entry under [wraps.*] is a wrap, named for the binary. Every other top-level key is a setting this file declares, so a mistyped one is an error rather than a wrap for a binary of that name.

Settings

KeyMeaning
shim_dirWhere secreq wrap drops PATH shims. ~/ is expanded. Set by secreq init.
wait_indicatorDefault true. Whether a blocked wrap prints a "waiting for approval" indicator to stderr: a spinner on a TTY, a timestamped line every 30s on a pipe. SECREQ_NO_WAIT_INDICATOR silences it per-invocation.
editorEditor id (code, cursor, zed, nvim) the rule editor's "Open in editor" button defaults to. Written when you pick one in the manager's Rules view.
providersProvider definitions. Optional; see providers.
secretsSecrets declared once under a name, with an optional per-secret cache ttl. See declared secrets.
sshSSH identities for the agent. Optional; see ssh-agent.
wrapsThe wraps themselves, keyed by binary name.

The schema pointer is the #:schema comment at the top of the file, not a key. TOML has no $schema convention, and a comment cannot collide with a setting.

Wraps

SettingTypeMeaning
reasonstringRationale shown in the consent prompt.
env_secretsarray (optional)Declared secret names to inject under those same environment-variable names. Names must match [A-Za-z_][A-Za-z0-9_]*; references and provider locators aren't accepted.
envobject (optional)Environment variables to inject. Each value is a secret://provider/locator reference or a secret://<name> naming a declared secret. Use this form for a different env name or an inline reference. Bare locators aren't accepted here.

A reference is secret://<provider>/<locator>: the provider is a scheme name (built-in or declared in providers), and the locator is everything after the first /. See providers.

A reference with no / after the scheme is a declared secret's name instead. The slash is the whole rule, so secret://op/Personal/GitHub/token and secret://github_token can never be read the same way.

At the Locator prompt you can paste the store's own reference instead of retyping the tail of it. 1Password's "Copy Secret Reference" gives you op://Vault/Item/field, quoted or not, and secreq wrap strips the scheme, the quotes and the whitespace, then tells you what it read.

Already-satisfied env vars

An env_secrets or env entry whose variable is already set in the calling environment to a non-empty value that isn't a secret://… marker, is skipped entirely. Nothing is resolved, and the child inherits what was already there.

If every entry is satisfied this way the run needs no consent at all and passes straight through, because secreq is releasing nothing. A partially satisfied wrap prompts only for what's missing. This is what keeps wrapped binaries cheap inside environments that pre-inject credentials: CI, a shell where you exported the token yourself, a nested secreq run.

Gate-only wraps

A wrap with neither env_secrets nor env is a gate-only wrap: invoking the binary still requires consent, but nothing is resolved or injected. Use it for a tool that manages its own credentials and has no secret for secreq to pass. op is the canonical case:

[wraps.op]
reason = "1Password vault access"
A wrap can inject nothing and still ask. Gate-only wraps put a consent step in front of a command without holding any secret for it.

Now every op read … pauses for a prompt showing the full command, the working directory and the caller tree, the "why am I getting this?" context the tool's own prompt omits. The evidence well's secret row gives way to a gate-only marker.

secreq wrap op --reason "1Password vault access" creates one directly, and the interactive flow offers it as the second option on the first question:

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

Wrapping a provider CLI is safe. If you gate op and use it as a secret://op/... provider, secreq won't double-prompt: it runs the provider with an internal marker that makes the wrapped op pass straight through. Only the op calls you make are gated, never the ones secreq makes to fetch a value for another wrap.

Declared secrets

A secret written inline in a wrap's env is a string, repeated in every wrap that needs it. Declaring it under secrets gives it a name to reference and a place for per-secret settings to live:

[secrets.GITHUB_TOKEN]
ref = "secret://op/Personal/GitHub Token/credential"
ttl = "15m"

[wraps.gh]
env_secrets = ["GITHUB_TOKEN"]

[wraps.hub]
env.GH_TOKEN = "secret://GITHUB_TOKEN"
SettingTypeMeaning
refstringThe secret://provider/locator this name stands for. Always a provider reference — a declaration can't name another declaration.
ttlstring (optional)How long the daemon may serve this secret from its cache. A count and a unit: 30s, 15m, 2h, 1d.

A name contains no /. Two names for one ref are fine as long as they agree on the ttl; a file where they disagree fails to load, because one reference is one cached value and so has one lifetime.

Changing a declaration's ref changes it for every wrap that references the name, which is the point. env_secrets removes the repeated env key and reference when the two names agree; env remains the explicit renaming form. secreq wrap gh --secret GITHUB_TOKEN writes the first form. secreq check reports an unknown declaration, a reference in env_secrets, or a name claimed by both forms.

$ 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│◆ Use a declared or previously used secret?│ ● secret://GITHUB_TOKEN│ ○ Define a new secret…└
┌ Wrap `gh`│◇ What should this wrap do?│ Inject secrets│◇ Use a declared or previously used secret?│ secret://GITHUB_TOKEN│◆ Inject under the declaration's own name `GITHUB_TOKEN`?│ ● Yes / ○ No└
┌ Wrap `gh`│◇ What should this wrap do?│ Inject secrets│◇ Use a declared or previously used secret?│ secret://GITHUB_TOKEN│◇ Inject under the declaration's own name `GITHUB_TOKEN`?│ Yes│◆ Add another env var?│ ○ Yes / ● No└
┌ Wrap `gh`│◇ What should this wrap do?│ Inject secrets│◇ Use a declared or previously used secret?│ secret://GITHUB_TOKEN│◇ Inject under the declaration's own name `GITHUB_TOKEN`?│ Yes│◇ Add another env var?│ No│◆ Reason (shown in consent prompt)│ (empty to skip)└
┌ Wrap `gh`│◇ What should this wrap do?│ Inject secrets│◇ Use a declared or previously used secret?│ secret://GITHUB_TOKEN│◇ Inject under the declaration's own name `GITHUB_TOKEN`?│ Yes│◇ Add another env var?│ No│◇ Reason (shown in consent prompt)│ GitHub API access│└ Wrapped `gh`. config: ~/.secreq/config.toml shim: ~/.secreq/shims/gh
A declared secret can be injected under its own name without typing the environment variable again. The resulting wrap records env_secrets = ["GITHUB_TOKEN"].

How long a value stays cached

The daemon caches a resolved value so a second command doesn't pay for a second provider call — on 1Password, a second biometric prompt. Without a ttl that value lives as long as the daemon, which is the default and what every secret does unless you say otherwise.

A ttl shortens it, for that secret only:

[secrets.prod_deploy_key]
ref = "secret://op/Work/Deploy/key"
ttl = "5m"

Five minutes after the value is fetched the daemon drops and scrubs it. The next command that needs it re-runs the provider, biometric prompt included — which is the trade you are making, and why this is per secret rather than a global setting. Set it on the credential you want re-confirmed, not on everything.

Two consequences worth knowing:

  • The approval is untouched. A wrap you approved and told secreq to remember stays approved. Only the value expires; the prompt does not come back.
  • The ttl follows the reference, not the name. A wrap that writes secret://op/Work/Deploy/key inline gets the same five minutes, because the daemon caches one value per reference.

A ttl is also the answer to a secret you rotate upstream: without one, the old value is served until the daemon restarts.

How approval is scoped

When you approve a wrap invocation, the decision is cached against the direct parent process, keyed on (wrap, ppid, parent start time).

ScenarioOutcome
Run gh from your zsh, approve, run gh again from that zshCache hit → no prompt.
Open a new terminal and run gh thereDifferent ppid → prompt.
An npm postinstall hook invokes gh through the shimDifferent ppid (npm) → prompt.
A pid is recycled after the original shell diedDifferent start time → prompt.

Descendants of a process you already approved for ride the same grant.

The start-time component is what makes this pid-recycle safe: (ppid, start_time) identifies exactly one process across its lifetime, so a new process inheriting the number gets a fresh prompt.

An approval has no TTL and no on-disk file. Its lifetime is bounded by two natural boundaries: the parent process's lifetime, and the daemon's. When the shell that approved a wrap exits, no process can share both its pid and its start time, so the entry becomes unreachable. When the daemon exits (secreq daemon stop, --force, or the two-hour idle timeout), the whole cache goes with it. Nothing artificial expires an approval in between, and a daemon restart is always the clean reset.

A secret's cache ttl is a different thing and does not touch this: when a value expires, the next command re-fetches it from the provider, but the approval it was released under still stands, so no prompt opens.

Two kinds of request never remember: secreq run, whose identity is fixed and would over-match, and SSH signatures, which have their own clock-bounded session grants.

Editing the file

secreq edit opens it in $EDITOR; secreq check and secreq doctor validate afterwards.

secreq wrap and unwrap edit the file for you but don't preserve hand-written comments through a write. Prefer them for adding entries, and secreq edit for surgical changes you want kept verbatim.