# Approval workflows

Protect an environment and writes to it stop landing. Instead they queue as
**change requests**: an admin reviews what is changing, and the change applies
the moment it has the approvals your policy asks for.

The part worth understanding first is what a reviewer sees. A change request
carries the **ciphertext the proposer already encrypted** — the same blob a
direct write would have stored — so a review shows *which secret is changing,
who proposed it, which version it replaces, and why*, and never the value.
Approving a change and reading a credential stay two different permissions,
which is exactly what you want from the person signing off on production.

> **Note:** Approval workflows are a **plan feature**. Your [plan & billing](/docs/guides/billing) page shows whether your plan includes them.

## Protect an environment

In the dashboard, open the environment and use the **Change approval** card:
pick how many approvals a change needs, and whether the proposer's own approval
may count toward it (off by default — separation of duties is the point).

From the CLI:

```bash
seekrit changes protection on --app store --env production --approvals 2
```

```bash
seekrit changes protection show --app store --env production
```

Turning protection on and off is admin-only, and **both directions are
audited** (`env.protection_enabled`, `env.protection_disabled`). Lifting the
gate is the security-significant half: it is the row to look for if a protected
environment turns out to have taken a direct write.

> **Note:** Protection is a product control, not a second key. It stops the API accepting a write and parks the ciphertext in a review queue — an admin can remove it. What it is *not* is a cryptographic seal: anyone holding a key grant for the environment could still decrypt what is already there.

## Propose a change

Nothing about writing changes. Set a secret exactly as you always would — from
the dashboard, the CLI, an SDK, or an MCP tool — and if the environment is
protected the write is queued instead of applied:

```bash
seekrit secrets set STRIPE_KEY sk_live_… --app store --env production -m "rotating after the incident"
```

```
queued for review: set STRIPE_KEY — needs 2 approvals (chg_…)
```

The HTTP answer is **202**, not 200, and the response body carries
`{ "queued": true, "changeRequest": … }`. Every seekrit client branches on
that and says so — the one thing a client must never do is report a write that
has not happened.

`-m` / `--note` is the explanation reviewers see. `seekrit secrets rm` and
`seekrit secrets restore` queue the same way.

## Review

```bash
seekrit changes list
seekrit changes show chg_…
seekrit changes approve chg_… -m "matches the rotation ticket"
seekrit changes reject chg_…
```

Or use **Change requests** in the dashboard sidebar.

The rules:

- **Reviewers are admins**, and must be signed in as a person. A service token
  cannot approve: whatever holds it can already write, so letting it approve
  would make the gate decorative.
- **One rejection closes the change.** There is no rejection quorum.
- **Approvals are counted per person.** The same admin approving twice does not
  satisfy a threshold of two.
- **The threshold approval applies the change in the same request** — there is
  no separate merge step to forget.
- The proposer can always **withdraw** their own change, even when they may not
  approve it.

## When the secret moves underneath

A change request records the secret version it was cut against. If that version
is no longer current when the change is approved — someone else's change applied
first, or the secret was deleted — seekrit marks it **superseded** and applies
nothing:

```
not applied: STRIPE_KEY is now at v9; this change was cut against v7
```

That is deliberate. Applying it would overwrite a value none of its reviewers
ever saw. Re-propose against the current value.

## What lands in the audit trail

A completed change leaves the whole chain, each row naming its own actor:

| Action | Written when |
| --- | --- |
| `change_request.opened` | a protected environment queued a write |
| `change_request.approved` | a reviewer approved (with the running count) |
| `change_request.rejected` | a reviewer rejected |
| `change_request.withdrawn` | the proposer or an admin took it off the queue |
| `change_request.applied` | the change was applied |
| `change_request.superseded` | the base moved, so it was not applied |

The ordinary `secret.created` / `secret.updated` / `secret.deleted` /
`secret.restored` row is written **as well**, carrying the change request's id —
so a secret's own history stays complete whether or not it went through review,
and the four-eyes evidence is a separate, attributable chain rather than a
reinterpretation of the same rows.

Version history credits the **proposer**, not the approver: they wrote the
ciphertext, and the approver never held a key.

## Notifications

Admins are emailed when a change is queued; the proposer is emailed when it is
applied, rejected, or superseded. Both are opt-out per user under [email
notifications](/docs/guides/notifications).

## Limits worth knowing

- Protection is **per environment**. Protect production; leave development
  alone.
- Reviewers are the org's **admins and owners** — there is no per-environment
  approver list yet.
- Settings are **snapshotted onto each change request** when it is opened, so
  loosening the policy later never releases something already in the queue.
- Taking protection off leaves queued changes queued. Withdraw them if they are
  no longer wanted.
