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
  1. 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:

Terminal
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 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:

SettingsScreen.tsx
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.

.squiggle/agent.json
{  "deny": { "pattern": "logout|delete|pay|transfer|send money", "enabled": true },  "budgets": { "escalations": 5, "actions": 300, "wallClockMs": 600000, "escalationTtlSec": 300 }}
LimitDefault
escalations3 decisions per run.
actions300 actions per run.
wallClockMs10 minutes per run.
escalationTtlSec5 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_evidence attaches a recording or screenshot to a pull request only after you add "prEvidence": true to .squiggle/config.jsonc.