> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Stop wrong, restricted, or malicious AI agent actions

> The last check before an AI agent does something it can't undo. Autonomy belongs to the action, not the agent.

export const HowDiagram = () => <>
    <svg className="cr-diagram cr-dia-wide" viewBox="0 0 1200 418" role="img" aria-labelledby="cr-dia-t cr-dia-d" preserveAspectRatio="xMidYMid meet">
      <title id="cr-dia-t">How ctrlrun works</title>
      <desc id="cr-dia-d">An action leaves the agents, tools and workflows you already run. Inside ctrlrun it is normalized into one action, decided against your rules, held for approval, reserved so it cannot run twice, executed, resolved and recorded. Only then does it reach your systems. An action with no rule is blocked, changed arguments void the approval, and an unknown outcome is never retried.</desc>
      <rect className="cr-dia-strip" x="1" y="1" width="1198" height="54" rx="6" />
      <text className="cr-dia-label" x="24" y="33">YOUR AGENTS, TOOLS AND WORKFLOWS</text>
      <text className="cr-dia-claim" x="390" y="34">An action is about to run.</text>
      <text className="cr-dia-note" x="628" y="34">Any model, any framework. Existing code stays as it is.</text>
      <path className="cr-dia-arrow" d="M600 56V83" />
      <path className="cr-dia-head" d="M595 82h10L600 90Z" />
      <rect className="cr-dia-box" x="1" y="92" width="1198" height="230" rx="6" />
      <text className="cr-dia-label" x="24" y="122">ctrlrun · THE EXECUTION BOUNDARY</text>
      <rect className="cr-dia-node" x="24" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="37" y="162">1</text>
      <text className="cr-dia-name" x="37" y="186">Normalize</text>
      <text className="cr-dia-gloss" x="37" y="206">one action, one id</text>
      <path className="cr-dia-arrow" d="M173 181h11" />
      <path className="cr-dia-head" d="M183 176.5l7 4.5-7 4.5Z" />
      <rect className="cr-dia-node" x="192" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="205" y="162">2</text>
      <text className="cr-dia-name" x="205" y="186">Decide</text>
      <text className="cr-dia-gloss" x="205" y="206">allow / ask / block</text>
      <path className="cr-dia-arrow" d="M341 181h11" />
      <path className="cr-dia-head" d="M351 176.5l7 4.5-7 4.5Z" />
      <rect className="cr-dia-node" x="360" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="373" y="162">3</text>
      <text className="cr-dia-name" x="373" y="186">Approve</text>
      <text className="cr-dia-gloss" x="373" y="206">bound to this action</text>
      <path className="cr-dia-arrow" d="M509 181h11" />
      <path className="cr-dia-head" d="M519 176.5l7 4.5-7 4.5Z" />
      <rect className="cr-dia-node" x="528" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="541" y="162">4</text>
      <text className="cr-dia-name" x="541" y="186">Reserve</text>
      <text className="cr-dia-gloss" x="541" y="206">claimed once</text>
      <path className="cr-dia-arrow" d="M677 181h11" />
      <path className="cr-dia-head" d="M687 176.5l7 4.5-7 4.5Z" />
      <rect className="cr-dia-node" x="696" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="709" y="162">5</text>
      <text className="cr-dia-name" x="709" y="186">Execute</text>
      <text className="cr-dia-gloss" x="709" y="206">your code runs</text>
      <path className="cr-dia-arrow" d="M845 181h11" />
      <path className="cr-dia-head" d="M855 176.5l7 4.5-7 4.5Z" />
      <rect className="cr-dia-node" x="864" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="877" y="162">6</text>
      <text className="cr-dia-name" x="877" y="186">Resolve</text>
      <text className="cr-dia-gloss" x="877" y="206">unknown is not failed</text>
      <path className="cr-dia-arrow" d="M1013 181h11" />
      <path className="cr-dia-head" d="M1023 176.5l7 4.5-7 4.5Z" />
      <rect className="cr-dia-node" x="1032" y="138" width="144" height="86" rx="5" />
      <text className="cr-dia-num" x="1045" y="162">7</text>
      <text className="cr-dia-name" x="1045" y="186">Record</text>
      <text className="cr-dia-gloss" x="1045" y="206">a receipt either way</text>
      <path className="cr-dia-div" d="M24 252H1176" />
      <text className="cr-dia-rule" x="24" y="278">No rule for it</text>
      <text className="cr-dia-rule-note" x="24" y="298">The action is blocked. Silence is never permission.</text>
      <text className="cr-dia-rule" x="418" y="278">Arguments changed after sign-off</text>
      <text className="cr-dia-rule-note" x="418" y="298">The old approval is void. A person signs again.</text>
      <text className="cr-dia-rule" x="812" y="278">Outcome unknown</text>
      <text className="cr-dia-rule-note" x="812" y="298">No retry until a person resolves it. Nothing runs twice on a guess.</text>
      <path className="cr-dia-arrow" d="M600 324V353" />
      <path className="cr-dia-head" d="M595 352h10L600 360Z" />
      <rect className="cr-dia-strip" x="1" y="362" width="1198" height="54" rx="6" />
      <text className="cr-dia-label" x="24" y="394">YOUR SYSTEMS</text>
      <text className="cr-dia-claim" x="252" y="395">The action arrives already checked.</text>
      <text className="cr-dia-note" x="560" y="395">Allowed by your rules, approved where you require it, and never run twice.</text>
    </svg>
    <svg className="cr-diagram cr-dia-narrow" viewBox="0 0 360 748" role="img" aria-labelledby="cr-dia-tn cr-dia-dn" preserveAspectRatio="xMidYMid meet">
      <title id="cr-dia-tn">How ctrlrun works</title>
      <desc id="cr-dia-dn">An action leaves the agents, tools and workflows you already run. Inside ctrlrun it is normalized into one action, decided against your rules, held for approval, reserved so it cannot run twice, executed, resolved and recorded. Only then does it reach your systems. An action with no rule is blocked, changed arguments void the approval, and an unknown outcome is never retried.</desc>
      <rect className="cr-dia-strip" x="1" y="1" width="358" height="76" rx="6" />
      <text className="cr-dia-label" x="16" y="25">YOUR AGENTS, TOOLS AND WORKFLOWS</text>
      <text className="cr-dia-claim" x="16" y="47">An action is about to run.</text>
      <text className="cr-dia-note" x="16" y="66">Any model, any framework. Existing code stays as it is.</text>
      <path className="cr-dia-arrow" d="M180 78V95" />
      <path className="cr-dia-head" d="M175 94h10L180 102Z" />
      <rect className="cr-dia-box" x="1" y="104" width="358" height="536" rx="6" />
      <text className="cr-dia-label" x="16" y="130">ctrlrun · THE EXECUTION BOUNDARY</text>
      <path className="cr-dia-spine" d="M28 150V420" />
      <rect className="cr-dia-node" x="28" y="142" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="164">1</text>
      <text className="cr-dia-name" x="58" y="164">Normalize</text>
      <text className="cr-dia-gloss" x="156" y="164">one action, one id</text>
      <rect className="cr-dia-node" x="28" y="184" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="206">2</text>
      <text className="cr-dia-name" x="58" y="206">Decide</text>
      <text className="cr-dia-gloss" x="156" y="206">allow / ask / block</text>
      <rect className="cr-dia-node" x="28" y="226" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="248">3</text>
      <text className="cr-dia-name" x="58" y="248">Approve</text>
      <text className="cr-dia-gloss" x="156" y="248">bound to this action</text>
      <rect className="cr-dia-node" x="28" y="268" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="290">4</text>
      <text className="cr-dia-name" x="58" y="290">Reserve</text>
      <text className="cr-dia-gloss" x="156" y="290">claimed once</text>
      <rect className="cr-dia-node" x="28" y="310" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="332">5</text>
      <text className="cr-dia-name" x="58" y="332">Execute</text>
      <text className="cr-dia-gloss" x="156" y="332">your code runs</text>
      <rect className="cr-dia-node" x="28" y="352" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="374">6</text>
      <text className="cr-dia-name" x="58" y="374">Resolve</text>
      <text className="cr-dia-gloss" x="156" y="374">unknown is not failed</text>
      <rect className="cr-dia-node" x="28" y="394" width="316" height="34" rx="4" />
      <text className="cr-dia-num" x="40" y="416">7</text>
      <text className="cr-dia-name" x="58" y="416">Record</text>
      <text className="cr-dia-gloss" x="156" y="416">a receipt either way</text>
      <path className="cr-dia-div" d="M28 446H344" />
      <text className="cr-dia-rule" x="28" y="470">No rule for it</text>
      <text className="cr-dia-rule-note" x="28" y="488">The action is blocked.</text>
      <text className="cr-dia-rule-note" x="28" y="504">Silence is never permission.</text>
      <text className="cr-dia-rule" x="28" y="534">Arguments changed after sign-off</text>
      <text className="cr-dia-rule-note" x="28" y="552">The old approval is void.</text>
      <text className="cr-dia-rule-note" x="28" y="568">A person signs again.</text>
      <text className="cr-dia-rule" x="28" y="598">Outcome unknown</text>
      <text className="cr-dia-rule-note" x="28" y="616">No retry until a person resolves it.</text>
      <text className="cr-dia-rule-note" x="28" y="632">Nothing runs twice on a guess.</text>
      <path className="cr-dia-arrow" d="M180 642V661" />
      <path className="cr-dia-head" d="M175 660h10L180 668Z" />
      <rect className="cr-dia-strip" x="1" y="670" width="358" height="76" rx="6" />
      <text className="cr-dia-label" x="16" y="694">YOUR SYSTEMS</text>
      <text className="cr-dia-claim" x="16" y="716">The action arrives already checked.</text>
      <text className="cr-dia-note" x="16" y="735">Allowed by your rules, approved where you require it.</text>
    </svg>
  </>;

**ctrlrun stops AI agents from taking wrong, restricted, or malicious actions in your workflows.**

Every action is checked against your rules before it runs. Allowed actions go through. Sensitive
ones wait for a person. Forbidden ones are blocked.

ctrlrun is a Python library that sits between an agent's decision to act and the call that acts.
A consequential action happens at most once, exactly as approved, and leaves a receipt, and when
the outcome is unknown, ctrlrun says so instead of guessing.

```bash theme={null}
pip install ctrlrun && ctrlrun demo
```

**Runs in production on a single file, or on Postgres across hosts.** SQLite is the default and
is production-grade on one host; Postgres is for many. Apache-2.0.

## How it works

ctrlrun stops an agent from taking an action your rules do not allow. Every action that leaves
your agents, tools and workflows is normalized into one action, decided against your policy,
held for a person where you require it, reserved so it cannot run twice, executed, resolved and
recorded. An action with no rule is blocked, arguments changed after sign-off void the approval,
and an outcome nobody knows is never retried on a guess.

<HowDiagram />

[Interactive walkthrough: follow one agent action through every check](/execution-boundary)

## Works with agents you can and can't modify

WhatsApp, Slack, Teams, Claude Code, Cursor, Codex, ChatGPT, OpenAI Agents. **Any AI agent you
have.** If it acts through your systems, it is checked.
[How it works](/docs/agents-you-cant-modify).

## Protect one function

ctrlrun wraps the call that has the consequence, and a YAML file says how much autonomy that
call gets. This is the whole integration for a function in your own process:

```yaml runnable theme={null}
schema: ctrlrun.policy/v2

actions:
  stripe.refund:
    effect: "refund:{payment_id}"
    rules:
      - when: { amount_gte: 0, amount_lte: 50000 }     # up to €500: autonomous
        decision: allow
      - when: { amount_gte: 0, amount_lte: 500000 }    # up to €5,000: a human decides
        decision: approve
      - decision: deny                                  # above that: never
```

```python runnable theme={null}
import ctrlrun


class Stripe:  # stands in for the real client so this block runs offline
    def refund(self, payment_id: str, amount: int) -> dict:
        return {"status": "succeeded"}


stripe = Stripe()


@ctrlrun.protect("stripe.refund", effect="refund:{payment_id}")
def refund(payment_id: str, amount: int) -> dict:
    return stripe.refund(payment_id, amount)


with ctrlrun.context(agent="refund-agent"):
    refund(payment_id="txn_1", amount=10000)  # €100: runs, and leaves a receipt
    try:
        refund(payment_id="txn_2", amount=200000)  # €2,000: waits for a human
    except ctrlrun.ApprovalRequired as pending:
        print("a human decides:", pending.request_id)
    else:
        raise SystemExit("the €2,000 refund ran without a human; the policy is not in force")
```

What the same function does next, and what stops it:

| The agent | ctrlrun |
| - | - |
| refunds €100 | runs it; one receipt |
| refunds €2,000 | raises `ApprovalRequired`; `ctrlrun approve <id>` from the shell lets it through |
| has €2,000 approved, executes €5,000 | `ApprovalMismatch`: the approval is bound to the action a human saw |
| refunds €20,000 | `ActionDenied`; no request is created |
| retries a refund whose reply was lost | `AmbiguousEffect`: the remote may have committed; a human or a reconcile hook decides |
| runs the same refund from two workers | one reserves `refund:txn_1`, the other gets `DuplicateEffect` |

The refund is the first example because everyone understands it; the same file protects a
`kubectl delete`, an IAM grant, a record deletion or an outbound email, and the
[cookbook](/docs/cookbook/index) has each of those as a runnable recipe.

## What the demo shows

Five ways an agent action goes wrong, and what stops each one, in under a second with no network.
The first scenario is the one that explains the product: a refund commits at the remote, the
reply is lost, the agent retries, and the retry is refused. The customer was refunded once.

```console theme={null}
$ ctrlrun demo
ctrlrun demo — five ways an agent action goes wrong, and what stops it.
Policy: refunds up to €1,000 are autonomous, up to €10,000 need a human, above that are denied.

1. Duplicate effect after a lost response

   refund €500  →  remote commits  →  response lost  →  effect: AMBIGUOUS
   agent retries the same refund
   ✗ BLOCKED — effect may already have committed; blind retry refused
   remote refund calls: 1
   only a human moves it on:  ctrlrun resolve refund:txn_1 --committed|--failed
```

The other four are approval mutation, two agents racing for one effect, approval replay, and an
agent trying to act outside what was delegated to it. [The execution boundary](/execution-boundary) draws
the same decisions without an install, or read the full transcript in the
[repository README](https://github.com/CTRLRun/ctrlrun#what-ctrlrun-demo-shows).

## What it does

<Columns cols={2}>
  <Card title="Approval binding" href="/docs/concepts/approval-binding">
    An approval is bound to the exact action; a mutated or replayed one is refused. Since v0.1.
  </Card>

  <Card title="One effect, once" href="/docs/concepts/effect-keys">
    One logical effect happens at most once, across threads, processes and hosts. Since v0.1.
  </Card>

  <Card title="Unknown is not failed" href="/docs/concepts/outcomes-and-ambiguous">
    An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry. Since v0.1.
  </Card>

  <Card title="Fail closed" href="/docs/concepts/fail-closed">
    An unknown action, a missing policy or a missing principal is denied. Since v0.1.
  </Card>

  <Card title="Authority and delegation" href="/docs/concepts/authority-and-delegation">
    Every principal needs a grant, delegation cannot widen one, and a grant bounds the total. Since v0.3.
  </Card>

  <Card title="Receipts" href="/docs/concepts/receipts-and-evidence">
    Every executed action leaves a portable JSON receipt of who, what and outcome. Since v0.1.
  </Card>
</Columns>

<Accordion title="Everything else it does (20 more)">
  <Columns cols={2}>
    <Card title="Per-action policy" href="/docs/reference/policy-yaml">
      One YAML file decides allow, approve or deny per action and argument. Since v0.1.
    </Card>

    <Card title="Operator CLI" href="/docs/reference/cli">
      Approve, deny, resolve, inspect and count from the shell, against any store. Since v0.1.
    </Card>

    <Card title="MCP gateway" href="/docs/guides/gateway-in-front-of-mcp">
      Every guarantee in front of an MCP tool server, with no agent changes. Since v0.2.
    </Card>

    <Card title="Reconciliation" href="/docs/guides/reconcile-automatically">
      A reconcile hook asks the remote what happened and resolves an AMBIGUOUS effect. Since v0.2.
    </Card>

    <Card title="Webhook approvals" href="/docs/guides/approvals-in-slack">
      Approval requests go to a webhook, such as Slack, and the answer comes back. Since v0.2.
    </Card>

    <Card title="OpenTelemetry export" href="/docs/guides/export-to-opentelemetry">
      One span per action, one span event per step; argument values are opt-in. Since v0.2.
    </Card>

    <Card title="Consumed identity" href="/docs/concepts/authority-and-delegation">
      A principal comes from a verified header or JWT; ctrlrun issues nothing. Since v0.3.
    </Card>

    <Card title="Runtime delegation" href="/docs/concepts/authority-and-delegation">
      A principal narrows its own grant at runtime; one revocation cuts the chain. Since v0.3.
    </Card>

    <Card title="Observe mode" href="/docs/concepts/observe-mode">
      Records what enforcement would have blocked, blocks nothing, and counts it. Since v0.3.
    </Card>

    <Card title="Verify" href="/docs/guides/verify-in-ci">
      Runs the guarantee catalogue against your policy and store; N/A is not a pass. Since v0.4.
    </Card>

    <Card title="The verified badge" href="/docs/verify/get-the-badge">
      A GitHub Action and a badge that means the declared guarantees pass. Since v0.4.
    </Card>

    <Card title="Framework adapters" href="/docs/get-started/three-ways-in">
      An approval routed through the framework's own interrupt; never a second path. Since v0.5.
    </Card>

    <Card title="Runs on one host or many" href="/docs/production/index">
      SQLite on one host, Postgres across hosts, the same guarantees either way. Since v0.6.
    </Card>

    <Card title="Postgres store" href="/docs/production/postgres">
      The same store on Postgres, graded by the suite written for SQLite. Since v0.6.
    </Card>

    <Card title="Versioned schema" href="/docs/production/migrations">
      Migrations run at open, forward only, and an unknown schema is refused. Since v0.6.
    </Card>

    <Card title="Recovery on restart" href="/docs/production/recovery">
      A dead worker's effect stays AMBIGUOUS until a human or a hook resolves it. Since v0.6.
    </Card>

    <Card title="Receipt chain" href="/docs/security/receipt-chain">
      Each receipt carries the hash of the one before; alteration is detected and named. Since v0.6.
    </Card>

    <Card title="Policy versioning" href="/docs/concepts/receipts-and-evidence">
      Every receipt names the policy hash and version that decided it. Since v0.6.
    </Card>

    <Card title="Control registry" href="/docs/reference/policy-yaml">
      Name the house controls an action satisfies, and receipts cite them. Since v0.6.
    </Card>

    <Card title="Data scope" href="/docs/reference/policy-yaml">
      Label arguments by data class and condition a rule on the labels present. Since v0.6.
    </Card>
  </Columns>
</Accordion>

## Three ways in

| You have | Use | Needs |
| - | - | - |
| Python in this process: a raw model call, a LangChain tool, a hand-rolled loop, a cron job | the `@protect` decorator | nothing beyond `pip install ctrlrun` |
| Tools behind an MCP server, in any language | the gateway, `ctrlrun gateway` | `pip install "ctrlrun[gateway]"` |
| A framework with its own approval interrupt, and a place where humans already answer | an adapter | the framework to have a human-in-the-loop primitive |

Most readers need the decorator. An adapter buys exactly one thing, routing an approval through
the framework's own interrupt, and a framework with no such primitive does not need one.
[Choosing between them](/docs/get-started/choosing) has the decision table.

On LangChain, ctrlrun is an official middleware integration: `CTRLRunMiddleware` is listed in
LangChain's [middleware integrations](https://docs.langchain.com/oss/python/integrations/middleware)
and gates every tool call the agent makes, tools you did not write included.
[Use the LangChain middleware](/docs/guides/langchain-middleware).

## Where it stands

* **Version 0.12.2**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14.
* **6,248 tests**, every version specified before it was written and every requirement mutation-tested.
* **32 guarantees you can check in your own setup**, with `ctrlrun verify` against your policy, on your store's backend, in a scratch store it creates.
* **One host: a file.** SQLite, no server, no ops. **Many hosts: Postgres**, the same guarantees, graded by the same suite.
* **Soaked for 20m 0s on postgres**: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. Nothing here establishes what only accumulates over days. [What it does not establish](https://ctrlrun.dev/docs/production/soak).
* **Each receipt carries the hash of the one before it**, so an alteration is detected and named.
* **Apache-2.0**, and the enforcement kernel stays open source. Releases carry PyPI provenance attestations from GitHub Actions.

**Not yet:**

* No external security audit. (optional, and no release waits for one)
* No third-party review of the kernel. (every review so far was run inside this project)
* No sector packs. (the policy templates are starting points, not a product)

## Built on this kernel

Two products run on ctrlrun and credit it on every page. [ctrl ai agents](https://ctrlaiagents.com), the hosted product for a person or a team, puts this boundary under any agent you buy or build, with the inbox, the receipts and the analysis in one dashboard. [ctrl payments](https://ctrlpayments.com) is the same boundary for money: every payment an agent attempts is allowed, held for a person, or refused before it leaves, and there is a receipt either way. The kernel that decides and refuses is this one, Apache-2.0, and the receipt format they write is the one documented here.

## Start here

<Columns cols={3}>
  <Card title="Protect your first action" icon="play" href="/docs/get-started/quickstart">
    Protect one function end to end and read the receipt. Ten minutes.
  </Card>

  <Card title="The execution boundary" icon="map" href="/execution-boundary">
    One action, five ways to stop it, drawn for any of twelve domains.
  </Card>

  <Card title="Cookbook" icon="book" href="/docs/cookbook/index">
    Refunds, deploys, IAM, deletions, email, MCP, LangGraph: each a recipe that runs.
  </Card>
</Columns>

<Columns cols={3}>
  <Card title="Three ways in" icon="signpost" href="/docs/get-started/three-ways-in">
    Decorator, gateway, adapter: what each covers and what each needs.
  </Card>

  <Card title="MCP" icon="plug" href="/docs/mcp/overview">
    The gateway in front of any MCP server, and this site as an MCP server.
  </Card>

  <Card title="Run it for real" icon="server" href="/docs/production/index">
    Which store, what a lost `COMMIT` does, what survives a crash, and what to watch.
  </Card>
</Columns>

<Columns cols={2}>
  <Card title="Why" icon="book-open" href="/docs/why">
    The five principles, in 700 words. The page people link to.
  </Card>

  <Card title="Outcomes and AMBIGUOUS" icon="circle-help" href="/docs/concepts/outcomes-and-ambiguous">
    The idea that explains the product: a timeout is not a failure.
  </Card>
</Columns>

## Ask your coding tool

This site is an MCP server. Add it to Cursor or any MCP client that takes an `mcpServers`
entry, and the assistant answers from these pages rather than from memory:

```json theme={null}
{
  "mcpServers": {
    "ctrlrun-docs": { "type": "http", "url": "https://ctrlrun.dev/mcp" }
  }
}
```

The server exposes one tool, a search across this documentation. When the site moves to its own
domain the URL moves with it; the current one is always in this block.

## Next

* [Why](/docs/why): what ctrlrun believes and why.
* [Install](/docs/get-started/install): what `pip install ctrlrun` puts on your machine, and what it does not.
* [How this is built](/docs/how-this-is-built): the discipline behind the guarantees.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.