Skip to content
in/guard/out
in/guard/out

Docs

Flow labels: where what an agent read may go

Most agent guardrails look at a tool call and ask whether it is allowed. in/guard/out flow labels ask a second question: given everything this run has read, may this data go there? Every run carries one label with two parts, and the label only gets stricter.

Trust follows who wrote what the run read. A web page, an inbound email, or a tool result that carried injected instructions lowers it, and tools that send, pay or write then refuse. Rooms follow who may see it. Reading an HR record puts the run in the hr room, and from then on its data may only go to the room’s members – by email, by Slack, or in the chat answer itself.

Both are decided without reading the content. A classifier asks whether a page looks malicious; the trust check only knows the run read one. So instructions written as ordinary prose still cannot drive a payment, and a salary the model paraphrases still cannot leave the room. You set it up per API key on the dashboard’s Protect page, from the tools that key’s agents actually used.

Set it up: the Protect page

Open API keys → Protect for the key your agent uses. You never type a tool name from memory: the page lists the tools that key’s agents actually called, with a suggested role for each – reads untrusted content, sends to the address in to, reads something confidential.

  1. Pick a risk. “Leak confidential data”, “act on instructions from a page or email”, “move money without a check”, “delete or run things”. A few plain questions – which tools read it, who may receive it, what happens otherwise – and the rules are written for you.
  2. Preview it. The draft is replayed over the key’s recent runs: which actions it would have blocked, which would wait for a person, which it can’t decide. Nothing is sent and nothing is enforced.
  3. Publish to watch. A watching policy only flags – even on a key in PREVENT mode – so you see what it would do on live traffic.
  4. Start enforcing when the flags match your intent. The rollout belongs to the policy, so the key’s other guardrails keep their own mode.

The page also checks the wiring: whether requests carry X-Guardrails-Session-Id (without it a room only lasts one request), whether subagents send their parent header, and whether Flow labels is on in Settings. A live policy whose setting is off is shown as not active, with a button to turn it on.

Later blocks feed back: in the Runs tab every room or trust finding has Why? and Allow in future, which edits the draft for you to preview and publish. A parked call can be approved once, or with Always allow this recipient.

About the preview: the log keeps PII screened, so a recipient is stored as a placeholder. in/guard/out also keeps the email’s domain – never the address – so a recipient at a domain the room doesn’t cover is decided as outside it, and one covered by *@domain as inside. Only a domain matched by exact addresses alone is reported as “can’t tell”, never guessed.

Trust: a web page can’t drive an action

With no configuration at all, trust is guessed from tool names. A tool rule (below) always wins over the guess, and "flow_defaults": false turns the guessing off.

DefaultWhich tools
Untrusted sourceA network-tier tool that does not send – fetch, http, browse, download, read_email. Reading it lowers trust.
EgressA send verb in the name (send, post, publish, share, upload, notify, reply, forward…) or a write, exec, delete or payment tool. Needs a trusted run; inside a room its destination is unknown, so it is refused.
NeitherEverything else – lookups, searches, reads of your own systems. No effect on the label.

Name your own tools when the guess is wrong. The levels are untrusted and trusted unless you set trust_levels (low → high), for example ["suspicious", "vendor", "internal"].

{
  "contracts": {
    "fetch_url":  { "trust": "untrusted", "egress": false },
    "read_email": { "trust": "untrusted", "egress": false },
    "send_email": { "requires_trust": "trusted", "egress": true },
    "pay_*":      { "requires_trust": "trusted", "egress": true }
  }
}

When the tool-result injection scan flags a result, the run drops to the lowest trust too. The probabilistic check feeds the deterministic one.

Confidential rooms

A room is a list of who may receive what was read from it – addresses or globs. A room can sit inside a wider one: data may move inward (internal → hr), never outward. A tool rule with enters puts the run in a room; argument selectors like files_read(path:/finance/*) match on the call’s arguments.

{
  "rooms": {
    "internal": { "members": ["*@acme.com"] },
    "hr":       { "members": ["people-ops@acme.com", "*@hr.acme.com"],
                  "parent": "internal" },
    "finance":  { "members": ["cfo@acme.com", "ap@acme.com"],
                  "on_breach": "approve" }
  },
  "contracts": {
    "read_hr_record":           { "enters": "hr" },
    "files_read(path:/finance/*)": { "enters": "finance" },
    "send_email":               { "destination": "$to" },
    "post_slack":               { "destination_room": "internal" }
  }
}

After read_hr_record, an email to people-ops@acme.com passes and one to an outside recruiter is refused. Four rules make a room a room:

  • Rooms intersect. A run that read both hr and finance may only send to people in both.
  • Unknown is public. Inside a room, a tool that sends data and has no declared destination is refused. Otherwise any undeclared tool would be a way out.
  • A rule replaces the guess. A tool rule is the whole contract for that tool, so add "egress": true to a sending tool you only gave a trust requirement.
  • Real addresses count. The model sees screened placeholders such as <EMAIL_ADDRESS_1>; membership is checked on the real address, and the stored violation keeps the placeholder.

Tool rules, field by field

contracts maps a tool name or glob to what it does. The first matching rule wins, top to bottom.

FieldValueWhat it does
entersroom or [rooms]Reading this tool’s output puts the run in the room. It stays there for the rest of the run.
sanitizersanitizer nameRedact this tool’s result before the model sees it, then relabel the read by the sanitizer’s permits.
trusttrust levelReading this tool’s output lowers the run’s trust to at most this level.
requires_trusttrust levelThe call is refused while the run’s trust is below this level.
destination"$arg" or a literalWhere the call sends data. "$to" means the value of the to argument, checked for membership.
destination_roomroom or "public"The call publishes to everyone in that room (a Slack channel, a wiki space).
egresstrue / falseOverride whether the call sends data out. Set by destination fields; set it explicitly when there is none.
spawntrueA subagent spawn. Allowed inside a room, because the child inherits the room.
on_breach"approve"Park a breach for a person instead of refusing it.

A call is checked against the label it would leave behind, including its own read. A tool that reads the finance file and emails it in one step is judged against the finance room.

Sanitizers: the declared way out

Work still has to get done. A sanitizer redacts data and may then release it into a wider room – names, numbers, email addresses, or your own patterns. It is the only way data leaves a room without a person approving it.

{
  "sanitizers": {
    "hr-redactor": {
      "permits":  { "from": "hr", "to": "internal" },
      "redact":   ["pii", "numbers", "emails"],
      "patterns": ["\\bband L\\d\\b"],
      "on":       ["tool_output", "answer", "child_return"]
    }
  },
  "contracts": {
    "read_hr_summary": { "enters": "hr", "sanitizer": "hr-redactor" }
  }
}

on says where it runs. On tool_output the result is cleaned before the model sees it, so the run enters internal instead of hr. On answer and child_return it cleans the text on its way to a reader or a parent run.

Redaction is irreversible – [REDACTED NUMBER], never a placeholder that PII restore would put back – and all or nothing: anything it cannot finish is withheld. A sanitizer never covers a tool’s own raw read, and the checkpoint API never relabels, because results never pass through it.

The chat answer is a destination

A model that read a salary can simply say it. Tell in/guard/out who reads the answers – the same audience for every answer on the key, or the end user of each request:

{ "answer": { "room": "internal" } }

# or, per request, from a multi-user app:
X-Guardrails-Reader: jordan.lee@acme.com

A reader outside the run’s rooms gets the answer redacted by a sanitizer that runs on answer. With no such sanitizer, the answer is withheld (PREVENT) or flagged (FIX). With neither setting, answers are not checked.

Subagents

A subagent starts with its parent’s label and keeps up with it, so delegating is never a way out of a room. Its text answer is its return: rooms the parent is not in go through a sanitizer on child_return, or the parent narrows into them. Set require_sanitized to withhold a raw return instead.

# the parent run
X-Guardrails-Session-Id: run-42

# every request the subagent makes
X-Guardrails-Session-Id: run-42-sub-1
X-Guardrails-Parent-Session-Id: run-42

{
  "contracts": { "spawn_agent": { "spawn": true } },
  "child_return": { "require_sanitized": true }
}

That gives the classic pattern: the main agent needs a conclusion from a record, not the record. A subagent reads it, its answer comes back cleaned, and the parent stays in internal. Inside a room a spawn tool is refused unless its rule says "spawn": true – set it only when your harness really sends the parent header.

Approvals: ask a person instead of refusing

Some data does leave the room – to an auditor, once. Add "on_breach": "approve" to the room or the tool rule, and a breach is parked rather than refused:

HTTP/1.1 422
X-Guardrails-Approval-Ids: c38e0550e6d44d1fbb8a4c87377cbfc5

{ "error": {
    "message": "Blocked by guardrails",
    "decision": "require_approval",
    "approvals": [{ "id": "c38e0550e6d44d1fbb8a4c87377cbfc5", "tool": "send_email" }],
    "violations": { "room_egress": [{ "tool": "send_email", "kind": "recipient",
                                      "rooms": ["finance"], "needs_approval": true }] },
    "feedback": { "message": "The action needs human approval before it runs. …" } } }

The call waits in the dashboard’s Runs tab. Approve it and the same call in the same run goes through once; a changed argument is a new call. The checkpoint API and MCP gateway use the same queue and return the ids under approvals. A request is only parked when every finding asked for a person – one approvable breach never launders another.

Headers

X-Guardrails-Session-IdNames the run. The label is stored per run, so it survives a client compacting its history. Without it the label is per request.
X-Guardrails-Parent-Session-IdMarks a subagent. It starts with its parent’s label, and its answer goes back through the checked return path.
X-Guardrails-ReaderWho reads this answer. Checked against the run’s rooms like an email recipient.
X-Guardrails-Approval-IdsResponse header: the approvals a refused request is waiting on.

Findings appear as untrusted_context and room_egress in violations, and every refusal carries feedback the agent can act on – it never names the correct value. The Runs tab shows each run’s rooms, trust and label history.

Start from a template

The Protect page’s risk flows build these from your own tools. The same four also exist as templates in its Advanced editor, for setting up a key before it has traffic – every value there is a placeholder, and publishing one unedited asks first.

A web page can’t drive an action

Untrusted reads lower trust; sending, paying and writing then need a trusted run.

HR records stay with HR

An hr room inside internal, recipient checks on email, and chat answers redacted for everyone else.

Finance data leaves only with a sign-off

A finance room whose breaches wait for a person instead of being refused.

Let a subagent read it

The child reads the record; only its cleaned answer reaches the parent.

What it does not do. A room controls what leaves through tools, the answer and subagents. It cannot stop a model from remembering, and it only knows a read was confidential because a rule says so. Name your confidential tools; the name guess covers trust, not rooms.

Keep reading

Set up rooms in the dashboard, not a text editor.

The Protect page builds these rules from the tools your agents actually use, and previews them on past runs before anything is enforced. We're running a limited demo – sign up and we'll get you in as soon as we can.