# Approvals (/docs/approvals)

Approvals put a human in front of the actions that need one. You write a
policy in plain English — no rule builder, no syntax — and every write your
agents make is judged against it. Most calls are allowed and nothing changes.
The ones your policy says to check stop, and you get a card.

Anything your policy never mentioned is decided by a **default behavior** you
choose — and on the default setting an ordinary write simply runs. That is
covered below, and it is the part worth reading before you rely on this.

**Approvals are off until you turn them on.** A new organization has no policy
and no gate; nothing is judged and nothing pauses.

The control surface is
[Dashboard → Approvals](https://app.corespeed.io/approvals).

## Three verdicts, and nothing else \[#three-verdicts-and-nothing-else]

Every gated call gets exactly one of them:

| Verdict   | What the agent sees                                   | What you see                        |
| --------- | ----------------------------------------------------- | ----------------------------------- |
| **Allow** | its result, immediately                               | nothing — it is in the activity log |
| **Ask**   | the call is held; it is told a human is deciding      | a card, and nothing has run yet     |
| **Deny**  | a refusal, at once — nothing ran and nobody was asked | no card — it is in the activity log |

Verified reads are not gated. A tool that only looks at something — a search,
a listing, reading a message — goes straight through, which is what keeps the
gate usable: your agent's ordinary work is not interrupted to confirm that
looking is allowed.

CoreSpeed decides what counts as a read, not the tool. Its own tools count, and
a vendor's tool counts once CoreSpeed has reviewed it. A tool on your
organization's own remote server, or a vendor tool not yet reviewed, is judged
even when it says it only reads. A read through an account whose owner shared
it with "require approval" waits for the owner too.

## Writing the policy \[#writing-the-policy]

Write sentences, not rules. The whole text is read, in context, for every
judged call:

```text
Sam handles support. Replies to customers who have written to us first are
fine. Ask me before anything goes to a public channel or an external company,
and before any refund over $100. Never post on our behalf about pricing,
funding, or anything that reads like a product promise.
```

Two things follow from it being TEXT rather than rules.

**Nothing is compiled.** Your words stay your words. A decision made last week
stays explainable by the text that was active last week — every save is a new
version, and the history is on the policy page.

**You can test a sentence before relying on it.** Test policy runs a
hypothetical call against your saved text, through the same evaluation the
gate uses, so you can see what a new sentence changes without waiting for an
agent to make that call.

### Your own policy, on top of the organization's \[#your-own-policy-on-top-of-the-organizations]

An organization admin writes the organization text — the ceiling everyone is
judged against. Any member can add their own text, and it can only make things
**stricter** for their own calls. Where the two disagree, the stricter verdict
wins. A member's text cannot loosen the organization's.

## When your policy is silent \[#when-your-policy-is-silent]

Some call falls outside everything you wrote. You pick a **default behavior**,
and the judge applies it wherever your sentences have no opinion:

| Setting                | An action nobody wrote about                               |
| ---------------------- | ---------------------------------------------------------- |
| **Relaxed**            | **runs**                                                   |
| **Balanced** (default) | **runs** — unless it is destructive, in which case it asks |
| **Strict**             | **asks you** — every write                                 |
| **Lockdown**           | **refused** — every write, without asking anybody          |

On Balanced, "destructive" is judged from what the operation and its arguments
would actually do — delete data, cause a loss nothing can undo. The tool
author's own annotations (`destructiveHint`) count as hints, not as an
automatic requirement to ask.

Your written rules take priority over the default: a sentence that addresses
the action decides it, whatever the default says. Two guards sit above every
verdict — a low-confidence answer becomes an ask, and an allow that looks like
prompt injection becomes an ask.

So on **Balanced**, an ordinary write your policy never mentioned goes through.
If that is not what you want, you have two ways to change it, and they are
different tools: raise the default to **Strict**, or write the sentence. The
sentence is usually better — it is specific, and a version records who wrote it
and when.

### When the judge cannot answer \[#when-the-judge-cannot-answer]

A failed evaluation — a timeout, an outage, a call too large to read, an
error — is not decided by the default behavior. A separate setting on the
policy page decides it, and its only options are **Ask** (the default) and
**Deny**. There is no failure path that lets a write run.

## Where a card reaches you \[#where-a-card-reaches-you]

The queue at [Dashboard → Approvals](https://app.corespeed.io/approvals) always
has it, and it is the only place the full call is shown. The other channels are
ways of being told:

* **Email** — the tool, when the request expires, and approve / deny links.
  A link opens the card on the dashboard, and your click there decides. The
  call's arguments are not in the mail; they stay behind your session.
* **Slack** — a card in your DM, or in one channel your organization picks,
  posted by the CoreSpeed bot in the Slack workspace your organization
  connects. An admin installs the workspace once, from the dashboard; each
  member then picks the Slack account that identifies them, which is where
  their DMs go and how their clicks are verified.

Every channel you enable gets the card, and any of them can end it. Whichever
you use first wins, and the action runs exactly once regardless. A Slack card
is rewritten in place to say who decided; an email that has already been sent
is not recalled, so the queue is the place to check what is still open.

An agent's own Slack tools cannot post a card, cannot change the bot's name or
picture, and cannot write into the channel you route approvals to. That
separation is deliberate: a card in that channel is always CoreSpeed asking.

## What your agent experiences \[#what-your-agent-experiences]

One thing, the same in every client: a call your policy wants you to see
comes back at once with an error-shaped receipt — \*"Approval required, not
run, call `manage__approval_wait` with this id"\* — and your agent calls that
tool. `approval_wait` blocks until you decide and then returns the original
tool's real result, or an error saying the action was denied or the card
expired. Nothing has to be taught; the receipt says exactly what to do next.
From the CoreSpeed CLI, `cs mcp call` exits with code `6` and prints the
`cs mcp call manage__approval_wait` command to run next.

How long `approval_wait` can hold is up to your client. Clients that send a
progress token (Claude Code does) get a progress notification every ten
seconds and hold one call for the whole wait; a client that times out simply
calls `approval_wait` again with the same id, which was in your agent's hands
before the waiting started. If the approved call already ran on the wait that
timed out, the new wait says so instead of running it twice; that result went
to the earlier wait, and Activity records the outcome.

The tool keeps its own name — there is no `_with_approval` variant and no
second path around the gate.

**Do not make the call again.** A fresh `tools/call` is a new request: it is
judged on its own, it can raise a second card, and approving both runs the
action twice. The receipt says so, and `manage__approval_wait` exists so there
is something else to do — one card, one id, one execution.

**The card lives as long as your agent waits.** If your agent stops waiting —
the model gives up, the client is closed — the card expires about thirty
seconds later and the queue says so: "no agent was waiting for the result".
Approving an abandoned card runs nothing; ask the agent again and a new card
appears. A card that nobody decides expires after an hour.

## Exactly once \[#exactly-once]

An approved card is a one-time credential for one frozen call, and the agent's
own waiting request is what runs it.

* The call is frozen when it is held — the **arguments you approve are the
  arguments that run**. They are shown in full on the card while it is open
  and cleared when it closes.
* Approving hands your decision to the agent request that is waiting; that
  request runs the call once and receives the result. Two Approve clicks at
  the same moment, or a Slack click racing a dashboard click, hand it over
  once; the second sees who decided.
* A denial can carry a reason, and the reason goes back to the agent.

## Who can decide \[#who-can-decide]

The person the card is addressed to: the member who made the call, or the
**owner** of the agent or API key that made it. An agent cannot approve its
own action. Organization admins can see every card in the queue, which is how
you audit what is waiting — seeing is not deciding.

## What the judge is shown \[#what-the-judge-is-shown]

Your policy, the tool's name and description, the annotations its author set
(read-only, destructive), who is calling and from which client, and how often
this caller has used the same TOOL recently (not the same arguments — a re-sent
identical call is a new call and a new judgement). Argument values are included
so a policy about *who* an email goes to can be applied — and if you would
rather they were not, turn off **Review tool arguments** on the policy page.
Your agent's conversation is never sent.

A call too large to judge is not judged: like every failed evaluation, it goes
to the Ask/Deny failure setting. It is never simply allowed.

## Test it before you rely on it \[#test-it-before-you-rely-on-it]

**Test policy**, on the policy page, runs a hypothetical call through the same
judgement the gate makes — the same evaluation path execution uses. Nothing is
frozen, nothing is executed, nothing lands in the queue. It tests saved text,
so save your edits first, then use it to see what changed.

## Related \[#related]

* [Activity](/docs/activity) — every decision is in the log, and a gated call
  carries its approval id.
* [Capability controls](/docs/capability-controls) — which tools exist at all,
  which is a different question from which ones need a signature.