# Safety and approvals

Your agent can’t log out, delete, pay or buy in your app unless you approve that one step. The block is in Squiggle’s runtime, not in a prompt, so no wording gets the model past it.

## The deny rail

Before any tap is delivered, Squiggle checks the control’s testID, label and text against a list of words. A match is refused, whoever sent it: a program, an answer to a decision, or a single `tap`. See the list and the limits in force with:

```shell
squiggle agent config
```

Output:

```text
agent config defaults — no .squiggle/agent.json
  ✔ deny rail: enabled
refuses
  logout | sign\s*out | delete | remove | destroy | pay | purchase | buy
  checkout | confirm | submit\s*order | deactivate | close\s*account
  escalation budget         3
  action budget           300
  wall-clock budget   10m 00s
  escalation TTL       5m 00s
```

## Mark your own dangerous controls

The word list can’t know that “Send” moves money, and it only reads one language. Mark a control as destructive in your app, and it is refused whatever it says:

```ts
import { agentHint } from '@squiggle.sh/react-native';

<Pressable {...agentHint({ id: 'delete-account', destructive: true })}>
  <Text>Delete account</Text>
</Pressable>
```

`agentHint` returns ordinary props: `id` becomes the testID, and `role` and `label` become the accessibility role and label. It works on any component that passes those props on.

## Tunable values

The `tune` tool is the only way your agent writes into your app’s state, and it can only set values your app declared with [`defineTunable`](https://squiggle.sh/docs/reference/sdk#definetunable), within the range you gave. It can’t edit other props or state. The deny rail checks each tunable’s name the same way it checks a testID, so a name that matches the word list is refused.

## Approve one step

When a run needs a blocked step, it stops and asks for approval. Only the `approve_action` tool can answer. It is marked as needing a person, so your client asks you before it runs, and your agent can’t approve its own step. In Studio, you approve it from the run log.

An approval covers that one step in that one run, and is used up when the step runs. It is recorded with who gave it. Approvals are never remembered, so the same step on the next run asks again.

## Change the limits

`squiggle agent config --init` writes the defaults to `.squiggle/agent.json`. Edit it to change the words or the limits; a key you leave out keeps its default. Studio and the CLI both read it.

```
{
  "deny": { "pattern": "logout|delete|pay|transfer|send money", "enabled": true },
  "budgets": { "escalations": 5, "actions": 300, "wallClockMs": 600000, "escalationTtlSec": 300 }
}
```

| Limit | Default |
| --- | --- |
| `escalations` | 3 decisions per run. |
| `actions` | 300 actions per run. |
| `wallClockMs` | 10 minutes per run. |
| `escalationTtlSec` | 5 minutes to answer a decision before the run gives up. |

> **Turning the rail off**
>
> Setting `enabled` to `false` lets your agent tap anything, including on a simulator signed in to a real account. A `pattern` that isn’t a valid regular expression never turns the rail off: Squiggle keeps the default list instead.

## What else keeps runs in check

- **One run per simulator.** Your agent and you never drive the same simulator at the same time.
- **Every action is logged** in the project’s history, marked as coming from your agent, next to what you ran from the terminal and Studio.
- **Every program run is recorded**, so a failure already has its replay.
- **Nothing is posted without you.** `publish_evidence` attaches a recording or screenshot to a pull request only after you add `"prEvidence": true` to `.squiggle/config.jsonc`.
