Reference
JSON Schemas
Point your editor at these and it will complete and validate secreq's config as you type. Add a $schema key to the file, or register the URL in your editor's JSON settings.
config.toml schema
The schema for your wrap configuration. Add a "#:schema" comment pointing here to get completion and validation while authoring config.toml.
URL/schemas/wraps.schema.jsonTop level
Configuration for secreq (~/.secreq/config.toml, or $SECREQ_HOME/config.toml). Wraps live under wraps, keyed by binary name; a secret declared once under secrets is referenced from any number of them as secret://<name>. See docs/wraps.md.
| Key | Type | Description |
|---|---|---|
| editor | string | Editor the rule editor's 'Open in editor' split-button opens by default (an editor id such as code, cursor, zed, or nvim). Machine-local, like shim_dir; written when you pick an editor in the Rules view of secreq view. |
| providers | { [key]: Provider } | Provider scheme definitions. Built-in providers (op, keychain on macOS, lastpass / pass on Unix) are available without an explicit entry; entries here override or add new schemes. |
| secrets | { [key]: SecretDecl } | Secrets declared once under a name, keyed by that name. A wrap references one as secret://<name> instead of repeating its provider reference, and the declaration is where a per-secret cache ttl lives. A name contains no /, which is what tells the two reference forms apart. |
| shim_dir | string | Directory where secreq wrap drops PATH shims. Set by secreq init. Supports a leading ~/. |
| ssh | { [key]: SshIdentity } | SSH identities served by the consent-gated SSH agent, keyed by identity name. Each identity stores its public key inline (not secret) so the agent can answer REQUEST_IDENTITIES without a resolve; the private key is a secret:// reference resolved only at SIGN time. |
| wait_indicator | boolean | Whether a wrap prints a 'waiting for approval' indicator to stderr while blocked on the consent prompt (spinner on a TTY, a timestamped line on a pipe). Defaults to true; set false to silence. The SECREQ_NO_WAIT_INDICATOR env var overrides this per-invocation. |
| wraps | { [key]: Wrap } | Per-binary wrap configuration, keyed by the binary name the shim is installed under. A wrap with neither env_secrets nor env is *gate-only*: consent is required before the binary runs, but nothing is injected. |
BatchRetrieve
Batched-retrieve: one command invocation resolves many secrets at once (e.g. op run -- printenv). Used automatically when a wrap's env_secrets and env together reference the same provider for two or more entries, cutting biometric prompts from N to 1. Protocol: per requested (name, locator), set env var name to env_value with {locator} substituted; spawn command; parse stdout as KEY=VALUE lines.
| Key | Type | Description |
|---|---|---|
| command* | string[] | argv to execute. The synthetic env entries are added to the child's environment; no placeholder substitution happens on command itself.At least 1 item |
| env_value* | string | Template for each synthetic env entry's value. {locator} is substituted per secret; the env-var name is the secret's name. For 1Password: "op://{locator}". |
FieldSpec
Schema for one field in a provider's store.fields.
| Key | Type | Description |
|---|---|---|
| default | string | null | — |
| optional | boolean | Sugar for required: false. |
| required | boolean | Default false |
Provider
A provider scheme. Required retrieve, optional store, optional retrieve_batch.
| Key | Type | Description |
|---|---|---|
| read | string[] | Legacy name for retrieve (the design doc §6 uses this name). Both are accepted; prefer retrieve.At least 1 item |
| retrieve | string[] | Argv template for fetching a secret. {locator} is substituted with the secret's locator; stdout is the value (one trailing newline stripped).Default []At least 1 item |
| retrieveBatch | BatchRetrieve | camelCase alias for retrieve_batch. |
| retrieve_batch | BatchRetrieve | null | Batched retrieve: one command invocation resolves many secrets at once (e.g. op run -- printenv). Used automatically when a wrap's env_secrets and env together reference the same provider for two or more entries, cutting biometric prompts from N to 1. |
| store | StoreCapability | null | How this provider persists a new value. Optional — a provider without it is retrieve-only. |
| write | StoreCapability | Legacy name for store. Both are accepted; prefer store. |
SecretDecl
One entry in the reserved secrets block: a provider reference declared **once**, under a name any number of wraps can reference as secret://<name>.
The name is what a TTL hangs on. Before this existed a secret had no declaration — it was a string repeated in every wrap that needed it — so there was nowhere for a per-secret setting to live.
| Key | Type | Description |
|---|---|---|
| ref* | string | The secret://provider/locator reference this name stands for. Always the direct form: a declaration naming another declaration would make resolution a graph walk with cycles to detect, for no reuse a second name buys. |
| ttl | string | null | How long the daemon may serve this secret from its in-memory cache before re-running the provider, as a count and a unit — 30s, 15m, 2h, 1d. Omit for the default, which is the daemon's own lifetime; any value here is a shortening, and shortening means the provider (and on 1Password, a biometric prompt) runs again once it elapses. |
SshIdentity
One SSH identity served by the agent. public_key is the inline OpenSSH public key (not secret); private_key is a secret://provider/locator reference resolved only at SIGN time.
| Key | Type | Description |
|---|---|---|
| private_key* | string | A secret://provider/locator reference to the private key, resolved only at SIGN time. |
| public_key* | string | Inline OpenSSH public key (ssh-ed25519 AAAA… comment). Answered to REQUEST_IDENTITIES without a resolve. |
| reason | string | null | Rationale shown in the consent prompt when this identity is used to sign. |
StoreCapability
How this provider persists a new value (currently exposed via custom CLIs the user may write that drive secreq programmatically — the public secreq CLI no longer exposes a store verb).
| Key | Type | Description |
|---|---|---|
| command* | string[] | Argv template. {field} placeholders are filled from caller-supplied inputs; {value} (argv mode) is the secret. Prefer stdin mode.At least 1 item |
| fields | { [key]: FieldSpec } | — |
| locator* | string | Template that builds the retrieve-side locator from the same field inputs. |
| value | string | How the secret reaches command. Omitted or "stdin" pipes it in on stdin, which is the default. Any other string (typically "{value}") opts into argv-substitution mode, where the secret appears in the process's command line and is readable by other users on Linux at the default hidepid=0. Prefer stdin.Default "stdin" |
Wrap
One per-binary wrap. env_secrets and env are optional: a wrap with neither is *gate-only* — consent is required before the binary runs, but nothing is injected (used to gate tools like op that have no secret to pass). Everything else is metadata.
| Key | Type | Description |
|---|---|---|
| env | { [key]: string } | Environment variables to inject. Each value is a secret://provider/locator reference; resolution happens at invocation time. Use this form to inject a declaration under a different name or to carry an inline reference. Omit (along with env_secrets) for a gate-only wrap. |
| env_secrets | string[] | Secret declaration names to inject as environment variables under those same names. Each entry must name a top-level [secrets.<name>]; it must also match [A-Za-z_][A-Za-z0-9_]*. Use env when the environment variable needs a different name or the reference is inline. |
| reason | string | null | Rationale shown in the consent prompt when this wrap is invoked. |
Raw schema
{
"$id": "https://craigory.dev/secreq/schemas/wraps.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"definitions": {
"BatchRetrieve": {
"additionalProperties": false,
"description": "Batched-retrieve: one command invocation resolves many secrets at once (e.g. `op run -- printenv`). Used automatically when a wrap's `env_secrets` and `env` together reference the same provider for two or more entries, cutting biometric prompts from N to 1. Protocol: per requested (name, locator), set env var `name` to `env_value` with `{locator}` substituted; spawn `command`; parse stdout as `KEY=VALUE` lines.",
"properties": {
"command": {
"description": "argv to execute. The synthetic env entries are added to the child's environment; no placeholder substitution happens on `command` itself.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"env_value": {
"description": "Template for each synthetic env entry's value. `{locator}` is substituted per secret; the env-var name is the secret's name. For 1Password: `\"op://{locator}\"`.",
"type": "string"
}
},
"required": [
"command",
"env_value"
],
"type": "object"
},
"FieldSpec": {
"additionalProperties": false,
"description": "Schema for one field in a provider's `store.fields`.",
"properties": {
"default": {
"type": [
"string",
"null"
]
},
"optional": {
"description": "Sugar for `required: false`.",
"type": "boolean"
},
"required": {
"default": false,
"type": "boolean"
}
},
"type": "object"
},
"Provider": {
"additionalProperties": false,
"anyOf": [
{
"required": [
"retrieve"
]
},
{
"required": [
"read"
]
}
],
"description": "A provider scheme. Required `retrieve`, optional `store`, optional `retrieve_batch`.",
"properties": {
"read": {
"description": "Legacy name for `retrieve` (the design doc §6 uses this name). Both are accepted; prefer `retrieve`.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"retrieve": {
"default": [],
"description": "Argv template for fetching a secret. `{locator}` is substituted with the secret's locator; stdout is the value (one trailing newline stripped).",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"retrieveBatch": {
"$ref": "#/definitions/BatchRetrieve",
"description": "camelCase alias for `retrieve_batch`."
},
"retrieve_batch": {
"anyOf": [
{
"$ref": "#/definitions/BatchRetrieve"
},
{
"type": "null"
}
],
"description": "Batched retrieve: one command invocation resolves many secrets at once (e.g. `op run -- printenv`). Used automatically when a wrap's `env_secrets` and `env` together reference the same provider for two or more entries, cutting biometric prompts from N to 1."
},
"store": {
"anyOf": [
{
"$ref": "#/definitions/StoreCapability"
},
{
"type": "null"
}
],
"description": "How this provider persists a new value. Optional — a provider without it is retrieve-only."
},
"write": {
"$ref": "#/definitions/StoreCapability",
"description": "Legacy name for `store`. Both are accepted; prefer `store`."
}
},
"type": "object"
},
"SecretDecl": {
"additionalProperties": false,
"description": "One entry in the reserved `secrets` block: a provider reference declared **once**, under a name any number of wraps can reference as `secret://<name>`.\n\nThe name is what a TTL hangs on. Before this existed a secret had no declaration — it was a string repeated in every wrap that needed it — so there was nowhere for a per-secret setting to live.",
"properties": {
"ref": {
"description": "The `secret://provider/locator` reference this name stands for. Always the direct form: a declaration naming another declaration would make resolution a graph walk with cycles to detect, for no reuse a second name buys.",
"pattern": "^secret://[^/]+/.+$",
"type": "string"
},
"ttl": {
"description": "How long the daemon may serve this secret from its in-memory cache before re-running the provider, as a count and a unit — `30s`, `15m`, `2h`, `1d`. Omit for the default, which is the daemon's own lifetime; any value here is a shortening, and shortening means the provider (and on 1Password, a biometric prompt) runs again once it elapses.",
"type": [
"string",
"null"
]
}
},
"required": [
"ref"
],
"type": "object"
},
"SshIdentity": {
"additionalProperties": false,
"description": "One SSH identity served by the agent. `public_key` is the inline OpenSSH public key (not secret); `private_key` is a `secret://provider/locator` reference resolved only at SIGN time.",
"properties": {
"private_key": {
"description": "A `secret://provider/locator` reference to the private key, resolved only at SIGN time.",
"pattern": "^secret://[^/]+/.+$",
"type": "string"
},
"public_key": {
"description": "Inline OpenSSH public key (`ssh-ed25519 AAAA… comment`). Answered to REQUEST_IDENTITIES without a resolve.",
"type": "string"
},
"reason": {
"description": "Rationale shown in the consent prompt when this identity is used to sign.",
"type": [
"string",
"null"
]
}
},
"required": [
"public_key",
"private_key"
],
"type": "object"
},
"StoreCapability": {
"additionalProperties": false,
"description": "How this provider persists a new value (currently exposed via custom CLIs the user may write that drive `secreq` programmatically — the public `secreq` CLI no longer exposes a `store` verb).",
"properties": {
"command": {
"description": "Argv template. `{field}` placeholders are filled from caller-supplied inputs; `{value}` (argv mode) is the secret. Prefer stdin mode.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"fields": {
"additionalProperties": {
"$ref": "#/definitions/FieldSpec"
},
"type": "object"
},
"locator": {
"description": "Template that builds the retrieve-side locator from the same field inputs.",
"type": "string"
},
"value": {
"default": "stdin",
"description": "How the secret reaches `command`. Omitted or `\"stdin\"` pipes it in on stdin, which is the default. Any other string (typically `\"{value}\"`) opts into argv-substitution mode, where the secret appears in the process's command line and is readable by other users on Linux at the default `hidepid=0`. Prefer stdin.",
"type": "string"
}
},
"required": [
"command",
"locator"
],
"type": "object"
},
"Wrap": {
"additionalProperties": false,
"description": "One per-binary wrap. `env_secrets` and `env` are optional: a wrap with neither is *gate-only* — consent is required before the binary runs, but nothing is injected (used to gate tools like `op` that have no secret to pass). Everything else is metadata.",
"properties": {
"env": {
"additionalProperties": {
"description": "A `secret://provider/locator` reference, or `secret://<name>` naming an entry in the top-level `secrets` block.",
"pattern": "^secret://[^/]+(/.+)?$",
"type": "string"
},
"description": "Environment variables to inject. Each value is a `secret://provider/locator` reference; resolution happens at invocation time. Use this form to inject a declaration under a different name or to carry an inline reference. Omit (along with `env_secrets`) for a gate-only wrap.",
"type": "object"
},
"env_secrets": {
"description": "Secret declaration names to inject as environment variables under those same names. Each entry must name a top-level `[secrets.<name>]`; it must also match `[A-Za-z_][A-Za-z0-9_]*`. Use `env` when the environment variable needs a different name or the reference is inline.",
"items": {
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
"type": "string"
},
"type": "array",
"uniqueItems": true
},
"reason": {
"description": "Rationale shown in the consent prompt when this wrap is invoked.",
"type": [
"string",
"null"
]
}
},
"type": "object"
}
},
"description": "Configuration for `secreq` (`~/.secreq/config.toml`, or `$SECREQ_HOME/config.toml`). Wraps live under `wraps`, keyed by binary name; a secret declared once under `secrets` is referenced from any number of them as `secret://<name>`. See docs/wraps.md.",
"properties": {
"editor": {
"description": "Editor the rule editor's 'Open in editor' split-button opens by default (an editor id such as `code`, `cursor`, `zed`, or `nvim`). Machine-local, like `shim_dir`; written when you pick an editor in the Rules view of `secreq view`.",
"type": "string"
},
"providers": {
"additionalProperties": {
"$ref": "#/definitions/Provider"
},
"description": "Provider scheme definitions. Built-in providers (`op`, `keychain` on macOS, `lastpass` / `pass` on Unix) are available without an explicit entry; entries here override or add new schemes.",
"type": "object"
},
"secrets": {
"additionalProperties": {
"$ref": "#/definitions/SecretDecl"
},
"description": "Secrets declared once under a name, keyed by that name. A wrap references one as `secret://<name>` instead of repeating its provider reference, and the declaration is where a per-secret cache `ttl` lives. A name contains no `/`, which is what tells the two reference forms apart.",
"type": "object"
},
"shim_dir": {
"description": "Directory where `secreq wrap` drops PATH shims. Set by `secreq init`. Supports a leading `~/`.",
"type": "string"
},
"ssh": {
"additionalProperties": {
"$ref": "#/definitions/SshIdentity"
},
"description": "SSH identities served by the consent-gated SSH agent, keyed by identity name. Each identity stores its public key inline (not secret) so the agent can answer REQUEST_IDENTITIES without a resolve; the private key is a `secret://` reference resolved only at SIGN time.",
"type": "object"
},
"wait_indicator": {
"description": "Whether a wrap prints a 'waiting for approval' indicator to stderr while blocked on the consent prompt (spinner on a TTY, a timestamped line on a pipe). Defaults to true; set false to silence. The SECREQ_NO_WAIT_INDICATOR env var overrides this per-invocation.",
"type": "boolean"
},
"wraps": {
"additionalProperties": {
"$ref": "#/definitions/Wrap"
},
"description": "Per-binary wrap configuration, keyed by the binary name the shim is installed under. A wrap with neither `env_secrets` nor `env` is *gate-only*: consent is required before the binary runs, but nothing is injected.",
"type": "object"
}
},
"title": "secreq config",
"type": "object"
}auto-rules schema
The schema for programmable auto-rules — the declarative decisions that gate secret release without prompting.
URL/schemas/auto-rules.schema.jsonTop level
Persisted auto-approve / auto-deny rules for secreq (~/.secreq/auto-rules.toml, or $SECREQ_HOME/auto-rules.toml). Owned by the consent daemon; clients normally don't edit the file directly.
| Key | Type | Description |
|---|---|---|
| rules | Rule[] | Ordered list of rules. Order does not affect precedence (deny-wins, then most-specific approve), but is preserved on read/write so hand-edits stay stable.Default [] |
Rule
One auto-decision rule. Exactly one of two shapes: declarative (decide + match, wasm absent) or wasm (wasm alone — the compiled module returns approve/pass/deny at evaluation time, so decide and deny_message must be absent).
| Key | Type | Description |
|---|---|---|
| created_at_unix | integer | Seconds since the Unix epoch at creation. Informational only.Default 0Minimum 0 |
| decide | RuleDecision | null | Direction a declarative rule fires when it matches. Among matching rules, any deny wins; otherwise the most-specific approve wins (a wasm rule that returns a decision counts as maximally specific). Forbidden on wasm rules. |
| declared_secrets | array | null | Subjects requested by a wasm module's subjects() export when the operator registered it. A request, not a grant: trained_secrets is the effective permission snapshot. Forbidden on declarative rules. |
| deny_message | string | null | Message printed to stderr on auto-deny and shown in the consent window's toast. Belongs to decide: deny: an approve rule refuses nobody, so a deny_message beside decide: approve is ignored, warned about by rule name in the daemon log, and removed the next time secreq writes the file. Forbidden outright on wasm rules — the module returns its own reason. |
| enabled* | boolean | false ⇒ rule is in the file but the evaluator skips it. Used for "pause this rule without losing the configuration". |
| id* | string | Stable identifier (12 random bytes as lowercase hex, generated by secreq when the rule is created). Never re-mint for an existing rule — it surfaces in the audit log. |
| match | RuleMatch | null | — |
| name* | string | Human label shown in the UI and the audit pill. |
| trained_secrets | string[] | Snapshot of env-var names the rule was created against. Declarative approvals require every requested name to be in this set. Wasm rules run when at least one requested name overlaps and can approve only overlapping names. Denies are never trained-scoped. An empty array is unbounded and requires explicit opt-in when registering wasm.Default [] |
| wasm | WasmRule | null | — |
| wraps | array | null | Optional consultation gate by ask wrap, applied before either rule kind is evaluated. Omitted or null means every wrap. When present, the rule is skipped unless the ask's wrap is listed; for wasm rules this happens before the module is instantiated. This composes with trained_secrets as an AND gate. A declarative rule's match.wrap is separate: wraps decides whether the rule is consulted at all, while match.wrap remains one clause in the rule's static match.At least 1 item |
RuleDecision
RuleMatch
The match clause. All present fields must match (logical AND); absent fields are unconstrained ("any"). wrap is required and exact; the rest are patterns: glob if they contain *, ?, or [, otherwise literal. A literal argv matches as a plain prefix; a literal cwd matches as a path-segment-aware prefix; a literal ancestor matches as a substring against the caller's executable path (friendlier for .app bundle names, and not self-reported).
| Key | Type | Description |
|---|---|---|
| ancestor | string | null | Pattern matched against each caller in the process tree; the clause matches if ANY caller satisfies it. Tested against the caller's executable path as the kernel reports it (e.g. /Applications/Cursor.app/Contents/MacOS/Cursor). Only when the kernel gives no exe does it fall back to the process's self-reported short name (typically the basename, like zsh or Cursor) and its joined command line. Preferring exe is what stops a process from satisfying ancestor: "Cursor.app" by putting that text in an argv it chose for itself. Substring match for literals, full-string glob for wildcards.Default null |
| argv | string | null | Pattern against the joined argv of the wrapped command (secreq reconstructs this as command.join(" ")). A literal matches as a plain prefix — argv has no segment structure to respect, so gh api is meant to match gh api --get /repos/x.Default null |
| cwd | string | null | Pattern against the requesting process's current working directory. A literal matches as a path-segment-aware prefix: it must cover the whole path or stop on a / boundary, so /Users/me/oss matches /Users/me/oss and /Users/me/oss/pkg but NOT /Users/me/ossuary. A trailing / on the pattern is optional and means the same thing. A glob is matched against the whole path.Default null |
| wrap* | string | Wrap name (exact match). |
WasmRule
Reference to a compiled wasm rule module, evaluated in the secreq sandbox (no WASI, fuel-metered, memory-capped). The module exports decide(ctx) returning approve, pass (rule does not match), or deny with a reason. A runtime error makes the rule not match — the ask falls through to the interactive prompt, never to an auto-approve.
| Key | Type | Description |
|---|---|---|
| path* | string | Path to the compiled .wasm module. Relative paths resolve against the directory containing auto-rules.toml; the canonical home is rules/<id>.wasm under the secreq root. |
| sha256* | string | Hex SHA-256 of the module bytes, recorded at registration and verified on every load. A mismatch refuses this rule (it can never fire) with a loud daemon-log error; other rules keep working. |
Raw schema
{
"$id": "https://craigory.dev/secreq/schemas/auto-rules.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"definitions": {
"Rule": {
"additionalProperties": false,
"description": "One auto-decision rule. Exactly one of two shapes: declarative (`decide` + `match`, `wasm` absent) or wasm (`wasm` alone — the compiled module returns approve/pass/deny at evaluation time, so `decide` and `deny_message` must be absent).",
"oneOf": [
{
"allOf": [
{
"not": {
"required": [
"wasm"
]
}
},
{
"not": {
"required": [
"declared_secrets"
]
}
}
],
"description": "Declarative rule: static decide + match clause.",
"required": [
"decide",
"match"
]
},
{
"allOf": [
{
"not": {
"required": [
"decide"
]
}
},
{
"not": {
"required": [
"match"
]
}
},
{
"not": {
"required": [
"deny_message"
]
}
}
],
"description": "Wasm rule: the module decides and may carry its confirmed `declared_secrets` request. `decide`/`match`/`deny_message` are forbidden — the decision (and any deny reason) is the module's return value.",
"required": [
"wasm"
]
}
],
"properties": {
"created_at_unix": {
"default": 0,
"description": "Seconds since the Unix epoch at creation. Informational only.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"decide": {
"anyOf": [
{
"$ref": "#/definitions/RuleDecision"
},
{
"type": "null"
}
],
"description": "Direction a declarative rule fires when it matches. Among matching rules, any deny wins; otherwise the most-specific approve wins (a wasm rule that returns a decision counts as maximally specific). Forbidden on wasm rules."
},
"declared_secrets": {
"description": "Subjects requested by a wasm module's `subjects()` export when the operator registered it. A request, not a grant: `trained_secrets` is the effective permission snapshot. Forbidden on declarative rules.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
],
"uniqueItems": true
},
"deny_message": {
"description": "Message printed to stderr on auto-deny and shown in the consent window's toast. Belongs to `decide: deny`: an approve rule refuses nobody, so a `deny_message` beside `decide: approve` is ignored, warned about by rule name in the daemon log, and removed the next time secreq writes the file. Forbidden outright on wasm rules — the module returns its own reason.",
"type": [
"string",
"null"
]
},
"enabled": {
"description": "`false` ⇒ rule is in the file but the evaluator skips it. Used for \"pause this rule without losing the configuration\".",
"type": "boolean"
},
"id": {
"description": "Stable identifier (12 random bytes as lowercase hex, generated by `secreq` when the rule is created). Never re-mint for an existing rule — it surfaces in the audit log.",
"type": "string"
},
"match": {
"anyOf": [
{
"$ref": "#/definitions/RuleMatch"
},
{
"type": "null"
}
]
},
"name": {
"description": "Human label shown in the UI and the audit pill.",
"type": "string"
},
"trained_secrets": {
"default": [],
"description": "Snapshot of env-var names the rule was created against. Declarative approvals require every requested name to be in this set. Wasm rules run when at least one requested name overlaps and can approve only overlapping names. Denies are never trained-scoped. An empty array is unbounded and requires explicit opt-in when registering wasm.",
"items": {
"type": "string"
},
"type": "array",
"uniqueItems": true
},
"wasm": {
"anyOf": [
{
"$ref": "#/definitions/WasmRule"
},
{
"type": "null"
}
]
},
"wraps": {
"description": "Optional consultation gate by ask wrap, applied before either rule kind is evaluated. Omitted or null means every wrap. When present, the rule is skipped unless the ask's wrap is listed; for wasm rules this happens before the module is instantiated. This composes with `trained_secrets` as an AND gate. A declarative rule's `match.wrap` is separate: `wraps` decides whether the rule is consulted at all, while `match.wrap` remains one clause in the rule's static match.",
"items": {
"type": "string"
},
"minItems": 1,
"type": [
"array",
"null"
],
"uniqueItems": true
}
},
"required": [
"id",
"name",
"enabled"
],
"type": "object"
},
"RuleDecision": {
"enum": [
"approve",
"deny"
],
"type": "string"
},
"RuleMatch": {
"additionalProperties": false,
"description": "The match clause. All present fields must match (logical AND); absent fields are unconstrained (\"any\"). `wrap` is required and exact; the rest are patterns: glob if they contain `*`, `?`, or `[`, otherwise literal. A literal `argv` matches as a plain prefix; a literal `cwd` matches as a path-segment-aware prefix; a literal `ancestor` matches as a substring against the caller's executable path (friendlier for `.app` bundle names, and not self-reported).",
"properties": {
"ancestor": {
"default": null,
"description": "Pattern matched against each caller in the process tree; the clause matches if ANY caller satisfies it. Tested against the caller's executable path as the kernel reports it (e.g. `/Applications/Cursor.app/Contents/MacOS/Cursor`). Only when the kernel gives no `exe` does it fall back to the process's self-reported short name (typically the basename, like `zsh` or `Cursor`) and its joined command line. Preferring `exe` is what stops a process from satisfying `ancestor: \"Cursor.app\"` by putting that text in an argv it chose for itself. Substring match for literals, full-string glob for wildcards.",
"type": [
"string",
"null"
]
},
"argv": {
"default": null,
"description": "Pattern against the joined argv of the wrapped command (`secreq` reconstructs this as `command.join(\" \")`). A literal matches as a plain prefix — argv has no segment structure to respect, so `gh api` is meant to match `gh api --get /repos/x`.",
"type": [
"string",
"null"
]
},
"cwd": {
"default": null,
"description": "Pattern against the requesting process's current working directory. A literal matches as a path-segment-aware prefix: it must cover the whole path or stop on a `/` boundary, so `/Users/me/oss` matches `/Users/me/oss` and `/Users/me/oss/pkg` but NOT `/Users/me/ossuary`. A trailing `/` on the pattern is optional and means the same thing. A glob is matched against the whole path.",
"type": [
"string",
"null"
]
},
"wrap": {
"description": "Wrap name (exact match).",
"type": "string"
}
},
"required": [
"wrap"
],
"type": "object"
},
"WasmRule": {
"additionalProperties": false,
"description": "Reference to a compiled wasm rule module, evaluated in the secreq sandbox (no WASI, fuel-metered, memory-capped). The module exports `decide(ctx)` returning approve, pass (rule does not match), or deny with a reason. A runtime error makes the rule not match — the ask falls through to the interactive prompt, never to an auto-approve.",
"properties": {
"path": {
"description": "Path to the compiled `.wasm` module. Relative paths resolve against the directory containing `auto-rules.toml`; the canonical home is `rules/<id>.wasm` under the secreq root.",
"type": "string"
},
"sha256": {
"description": "Hex SHA-256 of the module bytes, recorded at registration and verified on every load. A mismatch refuses this rule (it can never fire) with a loud daemon-log error; other rules keep working.",
"pattern": "^[0-9a-fA-F]{64}$",
"type": "string"
}
},
"required": [
"path",
"sha256"
],
"type": "object"
}
},
"description": "Persisted auto-approve / auto-deny rules for `secreq` (`~/.secreq/auto-rules.toml`, or `$SECREQ_HOME/auto-rules.toml`). Owned by the consent daemon; clients normally don't edit the file directly.",
"properties": {
"rules": {
"default": [],
"description": "Ordered list of rules. Order does not affect precedence (deny-wins, then most-specific approve), but is preserved on read/write so hand-edits stay stable.",
"items": {
"$ref": "#/definitions/Rule"
},
"type": "array"
}
},
"title": "secreq auto-rules config",
"type": "object"
}