Safety and approvals
For AI agents: this page is Markdown at https://squiggle.sh/docs/agents/safety.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.
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.
- logout
- sign out
- delete
- remove
- destroy
- pay
- purchase
- buy
- checkout
- confirm
- submit order
- deactivate
- close account
tap "Delete account" blocked'delete' matches the deny rail — this action is blocked below the model. If the run genuinely needs it, escalate with reason: needs_approval for a scoped single-use grant.
Blocked in the runtime, not in a prompt. The model cannot talk its way past it, because nobody asks the model.
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:
squiggle agent configagent config defaults — no .squiggle/agent.json ✔ deny rail: enabledrefuses 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 00sMark 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:
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, 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. |
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_evidenceattaches a recording or screenshot to a pull request only after you add"prEvidence": trueto.squiggle/config.jsonc.