Guardrails

Limit what an agent can break: least-privilege permissions, tools that check their inputs, human approval for risky actions, and wary use of tool results.

15 minLesson 6 of 6
On this page
  1. The idea: limits the model cannot talk its way past
  2. Permissions: give the least that works
  3. Tool limits: check the arguments
  4. Human approval for risky actions
  5. The gate in code
  6. Text from tools is untrusted
  7. Try it yourself
  8. Common mistakes
  9. Check yourself
  10. What to practise next

A developer asks a coding agent to clear out the project's temporary folder. The instructions say, in capitals, "BE CAREFUL. ONLY DELETE FILES IN tmp/." The agent builds the path from two pieces, one of them is empty, and the delete command runs on the project folder itself. A week of uncommitted work is gone.

The instruction was clear. The model even agreed with it. But an instruction is a request, and the delete tool accepted any path it was given. A guardrail would have been a delete tool that refuses any path outside tmp/, no matter who asks or why.

The idea: limits the model cannot talk its way past

Everything a model writes, including tool requests, comes from predicting text. It can misread, be tricked by text in a tool result, or follow a plausible but wrong chain of reasoning. You cannot make that chance zero by writing better instructions.

So put the limits in code, outside the model, where they hold no matter what the model writes. Think of them in four layers, from the outside in:

  1. Permissions. Which tools exist at all, and what the credentials behind them can reach.
  2. Tool limits. What each tool accepts: which paths, which amounts, which recipients.
  3. Human approval. Which actions wait for a person to say yes.
  4. Untrusted input. How much you let text from tool results steer later actions.

Permissions: give the least that works

The safest tool is the one the agent does not have. An agent that answers questions about orders needs to read orders. It does not need to delete customers, so that tool should not be on its list, and the database account behind it should be read-only. Then even an attack that fully fools the model has nothing to call.

The same goes for credentials. If the agent's API key can do everything your admin account can, every bug is an admin bug. Give the agent its own key, scoped to the few things its tools need, so you can also see in the logs which actions came from it.

Tool limits: check the arguments

Allowing a tool is not all or nothing. Tool Use showed tools that reject bad input so the model can fix it. Guardrails use the same place to reject dangerous input:

  • A file tool only accepts paths inside the project's tmp/ folder, after resolving .. and links.
  • A refund tool accepts amounts up to the order's value, never more.
  • An email tool only sends to addresses already on the customer's record.
  • A code-running tool runs inside a sandbox with no network and no access to your files.

Add rate limits too: no more than 10 refunds an hour, say. A rate limit does not stop one bad action, but it stops a confused agent from repeating it a hundred times.

Human approval for risky actions

Sort every tool by what happens if it is used wrongly:

RiskExamplesDefault
Read onlylook up an order, search docsrun automatically
Small or easy to undoa refund under 50, a draft replyrun, and log it
Costly, external or permanentlarge refunds, sending email, deleting, publishingask a person first
Not needed for this jobdelete a customerno tool at all

Here is a policy gate doing that sorting for a support agent. Every tool request passes through it before anything runs.

A policy gate checks each actionUse the arrows or ← → keys
rule = policy[call.tool]if no rule: blockif rule is read: allowif money and amount <= autoLimit: allowotherwise: ask a person
  1. actions
    requestruleoutcome
    getOrder(1042)readallowed
    ran
    1
    asked a person
    0
    blocked
    0

    1The agent wants to look up order 1042. Reading changes nothing, so the gate lets it run without asking anyone.

  2. actions
    requestruleoutcome
    getOrder(1042)readallowed
    refundOrder(1042, 30)money, 30 ≤ 50allowed
    ran
    2
    asked a person
    0
    blocked
    0

    2A refund of 30 for a damaged item. It is money, but under the automatic limit of 50, so it runs and is logged.

  3. actions
    requestruleoutcome
    getOrder(1042)readallowed
    refundOrder(1042, 30)money, 30 ≤ 50allowed
    refundOrder(2210, 400)money, 400 > 50approved
    ran
    3
    asked a person
    1
    blocked
    0

    3A refund of 400 for a double charge. Over the limit, so the gate pauses and shows a person the order and the amount. They check the payment records and approve.

  4. actions
    requestruleoutcome
    getOrder(1042)readallowed
    refundOrder(1042, 30)money, 30 ≤ 50allowed
    refundOrder(2210, 400)money, 400 > 50approved
    deleteCustomer(sam@…)noneblocked
    ran
    3
    asked a person
    1
    blocked
    1

    4A customer asked to close their account, and the agent tries deleteCustomer. That tool is not in the policy, so it is blocked. The error goes back to the agent, which tells the customer a person will handle it.

  5. actions
    requestruleoutcome
    getOrder(1042)readallowed
    refundOrder(1042, 30)money, 30 ≤ 50allowed
    refundOrder(2210, 400)money, 400 > 50approved
    deleteCustomer(sam@…)noneblocked
    sendEmail(backup@example.net)externaldeclined
    ran
    3
    asked a person
    2
    blocked
    1

    5One customer message hid a line telling the assistant to email the customer list to an outside address, and the agent tries. Sending is external, so a person sees the recipient, does not recognise it, and declines.

  6. actions
    requestruleoutcome
    getOrder(1042)readallowed
    refundOrder(1042, 30)money, 30 ≤ 50allowed
    refundOrder(2210, 400)money, 400 > 50approved
    deleteCustomer(sam@…)noneblocked
    sendEmail(backup@example.net)externaldeclined
    ran
    3
    asked a person
    2
    blocked
    1

    6Five requests: three ran, one was blocked, one was declined. The person was asked only twice, both times about something worth a look. Asking about every lookup would have trained them to click yes.

1 / 6

The gate in code

The gate sits in the agent loop where the tool used to run directly. askHuman shows the request to a person and returns their yes or no.

const policy = {
  getOrder: { risk: "read" },
  refundOrder: { risk: "money", autoLimit: 50 },
  sendEmail: { risk: "external" },
};
 
function checkAction(call) {
  const rule = policy[call.tool];
  if (!rule) return { decision: "block", reason: `${call.tool} is not on the allowed list` };
  if (rule.risk === "read") return { decision: "allow" };
  if (rule.risk === "money" && call.input.amount <= rule.autoLimit) {
    return { decision: "allow" };
  }
  return { decision: "ask" };
}
 
function guardedRun(call, runTool, askHuman) {
  const { decision, reason } = checkAction(call);
  if (decision === "block") return { error: `Blocked: ${reason}.` };
  if (decision === "ask" && !askHuman(call)) {
    return { error: "A person declined this action. Do not retry it." };
  }
  return runTool(call);
}
policy = {
    "get_order": {"risk": "read"},
    "refund_order": {"risk": "money", "auto_limit": 50},
    "send_email": {"risk": "external"},
}
 
def check_action(call):
    rule = policy.get(call["tool"])
    if rule is None:
        return {"decision": "block", "reason": f"{call['tool']} is not on the allowed list"}
    if rule["risk"] == "read":
        return {"decision": "allow"}
    if rule["risk"] == "money" and call["input"]["amount"] <= rule["auto_limit"]:
        return {"decision": "allow"}
    return {"decision": "ask"}
 
def guarded_run(call, run_tool, ask_human):
    check = check_action(call)
    if check["decision"] == "block":
        return {"error": f"Blocked: {check['reason']}."}
    if check["decision"] == "ask" and not ask_human(call):
        return {"error": "A person declined this action. Do not retry it."}
    return run_tool(call)

Two details matter. Unknown tools are blocked by default: the policy lists what is allowed, not what is forbidden, so a new tool is denied until someone decides. And a block or a refusal goes back to the model as an error it can read, so it can tell the user instead of retrying.

Text from tools is untrusted

The sendEmail request in the tracer did not come from the user. It came from text inside a customer message the agent had read with a tool. Prompt Injection covers how this works. For guardrails, the lesson is this: once an agent has read text you do not control, such as web pages, emails, documents or another system's output, treat its next actions as possibly steered by whoever wrote that text.

In practice, you tighten the gate after untrusted reads. An agent that has just read a web page should not send messages or touch private data without approval, even if those tools run freely at other times. The gate is where you keep apart the dangerous trio from that lesson: untrusted text, private data and a way to send data out.

Try it yourself

Try it: Set the policy for an assistant

An assistant manages a person's calendar and email. It has these tools. For each, choose: run automatically, run and log, ask first, or remove. Give one reason each.

Show a sample answer
  • readCalendar: run automatically. Reading changes nothing.
  • createEvent: run and log. It only touches the user's own calendar and is easy to undo.
  • sendEmail: ask first. It reaches other people and cannot be unsent. Show the recipients and the full text.
  • deleteEmail: ask first, or limit it to moving mail to a trash folder that empties after 30 days, which makes it reversible.
  • readWebPage: run automatically, but after it runs, treat the rest of the session as untrusted and require approval for any send.
  • forwardAllEmail: remove. No everyday task needs it, and it is exactly what an attacker would want: a silent, permanent copy of everything.

The pattern: decide by what a wrong call would cost, not by how often the tool is useful.

Common mistakes

Check yourself

Quick check

An agent only answers questions about orders. Which change protects best against it ever deleting data?

Why should the policy list allowed tools rather than forbidden ones?

An agent reads a web page, then asks to send an email. What is the safest default?

What to practise next

Take an agent or AI feature you use and list its tools with the worst thing each could do. Then add those worst cases to your evals, and read Privacy and Security for what to keep out of AI tools in the first place.