Sequence policies
Overview
Section titled “Overview”Many risky agent behaviours look harmless one step at a time. Reading .env is normal. So is calling curl. Reading .env and then calling curl is how secrets leave a laptop. A sequence policy is a rule over the order of an agent’s actions. It fires when a pattern of tool calls and user turns happens within one session.
Shield360 checks sequence policies in three places:
- Coding-agent hooks check each tool call before it runs. They compare it with a small per-session history kept on the developer’s computer.
- The MCP gateway checks each gateway tool call before it runs. It compares it with a per-session window of the agent’s recent calls.
- The after-the-fact scan runs every ten minutes. It rebuilds recent sessions from coding-agent traces and from the gateway’s call log, and records any violation that live enforcement could not see.
All three use the same engine. It is implemented natively in Go (the CLI) and in TypeScript (the platform). Both load one shared definitions file and are held to identical answers by a shared test corpus.
Violations are listed on the Violations page (open Govern → Policies & rules → Policies and select Violations), together with sensitive-data, AI-tool and prompt-injection decisions (see Violations). They are counted on the Govern → Compliance program overview and on the Home Compliance card, and each one raises a policy.violation alert.
Built-in policies
Section titled “Built-in policies”| Policy | Fires when | Severity | Needs |
|---|---|---|---|
secret-then-egress | A secret-like file is read (.env, SSH keys, cloud credentials, *.pem, kubeconfig, a keychain, terraform.tfstate…) or a credential command runs (gh auth token, security find-generic-password…), and a later call sends data to an outside destination: one that is neither internal nor allowed. It fires on the first such call after the read. It ends when the developer approves in a reply (“yes”, “go ahead”, “try again”), or after 20 more events. Test fixtures are not secrets. See Secret, then egress in detail. | critical | — (user turns optional) |
destructive-without-confirmation | A destructive command runs with no confirming user turn in the six events before it. Destructive commands include rm -rf, git push --force or +refspec, git reset --hard, DROP/TRUNCATE, DELETE without WHERE, terraform destroy, kubectl delete, helm uninstall, cloud delete APIs, and MCP tools named delete/drop/destroy/purge… A user turn confirms when it is an affirmative reply (“yes”, “go ahead”, “lgtm”…) or itself asks for the destructive action (“delete the old branches”). | warning | user turns |
taint-to-external | Data from one tool’s result appears in the outbound arguments of a later call to a destination that is not allowlisted. Matching uses keyed fingerprints, never content (see Data flow). | critical | tool results |
excessive-writes | The 26th write action since the last user turn is attempted. It fires once per run of writes. | warning | user turns |
privilege-change | sudo, chmod 777 or setuid, chown root, usermod/useradd, IAM attach/put/create, add-iam-policy-binding, (cluster)role bindings, or MCP tools that grant, attach or add permissions, roles, collaborators or members. | warning | — |
injection-then-sensitive | Prompt injection was detected in something the agent read, and a later call sends data out, deletes something, changes privileges or reads a secret. | critical | tool results |
Every built-in starts on, in warn. It records the violation and raises an alert, but stops nothing. You can switch each one off, or set it to block, under Sequence policies in Govern → Policies & rules → Policies. Warn lets you see what block would stop before you turn block on.
Where a runtime cannot see what a policy needs, the policy is not evaluated there live. The gateway cannot see user turns. Gemini CLI and Windsurf hooks cannot see tool results. The scan still checks these policies afterwards.
| Mode | Live (hooks and gateway) | Scan |
|---|---|---|
| Off | Not evaluated | Not evaluated |
| Warn | The call goes ahead. The violation is recorded and alerted. | Recorded and alerted |
| Block | The call is refused with a safe message wherever the agent lets Shield360 refuse it. Elsewhere it is recorded as a warning, marked “block not possible here”. | Recorded as a warning. The call has already happened. |
When a call is blocked it never runs, and it is kept in the session history as blocked. Every blocked attempt is still recorded as a violation. The built-ins ignore blocked calls when they look for the “first” egress, count writes or count the 20 events after a secret read, so retrying does not wear the policy out.
Secret, then egress in detail
Section titled “Secret, then egress in detail”This built-in protects against the classic exfiltration pattern: an agent reads a credential and then sends it somewhere. It is tuned so that normal development does not trip it.
What counts as a secret read. Reading a secret-like path (with a read tool, or as a word of a shell command such as cat ~/.aws/credentials), or running a credential command. These are not secrets:
- Examples and templates:
.env.example,.env.sample,.env.template,*.example,*.sample,*.dist, and public keys (*.pub). - Test fixtures: files under a
test/,tests/,testdata/,fixtures/,__fixtures__/,__mocks__/,__tests__/,spec/ore2e/folder, and files named likeserver.test.pemorkey.fixture.json. For example,services/api/test/.env.testis a fixture. A.env.testat the root of a project is still a secret. - Home-directory credentials are never fixtures, wherever they sit: anything under
.ssh/,.aws/,.config/gcloud/,.azure/,.kube/or.gnupg/,id_rsa-style keys, keychains,.netrc,.git-credentials,.pgpassand.vault-token. A key copied totests/.ssh/id_rsais still treated as a key. - The fixture check starts below your home folder and the project folder in it, so a project that happens to be called
test(~/test/.env) is not treated as a fixture folder.
The trade-off: a real credential that someone saves inside a test folder (for example a live API key in test/fixtures/.env) is not flagged by this policy. The taint-to-external policy still follows that file’s lines and tokens if they are sent out, and the agent guard’s secret scanning still applies.
What counts as an outside destination. Shield360 names where each call goes, as a host or a host and up to two path segments (an owner and a repository):
- URLs in the arguments:
curl https://app.example.com/api/xgoes toapp.example.com/api/x. git push: the remote’s URL. The hook reads the repository’s remotes and the branch’s push remote from the local git configuration in the agent’s working folder. It never contacts the network.git push,git push origin mainandgit push https://…are all resolved. A local path orfile://remote is this computer.gh:github.com(or--hostname), with the repository from--repo/-R, fromgh api repos/<owner>/<repo>/…ororgs/<org>/…, or else the repository the working folder is in.gh gist …goes togist.github.com.gh repo createandgh repo forkwithout an owner go to the developer’s own account, named asgithub.comonly, so an organization entry does not allow them.- Email addresses and the MCP or gateway service’s own address, if known.
A call whose destination cannot be named, such as git push to a remote Shield360 could not read, still counts as outside.
Approving a call. When a call is blocked, the agent is told why and what to do: “Data was sent to an outside destination after reading .env.local in services/api” or “after running gh auth token”. The message names the file (base name and up to two folders) or the command, never the content. If the developer expects the call, they reply with an approval such as “yes”, “go ahead”, “try again”, “retry”, “continue” or “approved”, and the next attempt goes ahead. A new secret read after the approval starts the policy again. Under minimal content capture, the message says only “after a secret read”.
The 20-event limit. After 20 further calls and user turns (blocked calls not counted), the policy no longer links the read to a later call. A typical agent turn makes 5 to 15 calls, so this covers “read the secret, then act on it straight away,” the pattern exfiltration follows. A model keeps what it read in its context, so this limit is a usability trade-off, not a risk reduction. Beyond it, taint-to-external still finds a secret’s lines or tokens sent to an outside destination within the 64-event history, and an agent that pads its work with 20 harmless calls before sending data is a pattern to review in the violations feed.
Where user turns are not seen. The policy declares user turns optional (the # optional: user_turns comment line). The MCP gateway, which cannot see a person’s turns, still evaluates it, without the approval.
Subagents. Claude Code runs subagents (the Agent or Task tool) in the same session as their parent. Hook events fired inside a subagent carry an agent_id, so the hook keeps each subagent’s calls apart: a call is decided against the main conversation’s events plus, for a subagent, its own. A secret read by one subagent does not hold back the parent or the other subagents. A secret read by the main conversation does hold back its subagents. What a subagent hands back reaches the parent as the Agent call’s result, which taint-to-external still follows. Other agents (Cursor, Codex) give subagents their own session. The after-the-fact scan still treats a Claude Code session as one, so it can record a warning for a subagent’s read followed by the parent’s call.
Claude Code’s own coordination tools (Agent, Task, SendMessage, TodoWrite, ListAgents, TaskOutput, plan-mode and similar tools) stay on the computer. They are never outbound, even though names like SendMessage contain “send”. An MCP tool such as mcp__slack__send_message is still a message out.
Enforcement coverage
Section titled “Enforcement coverage”Coding agents
Section titled “Coding agents”| Agent | User turns | Refused before a tool call | Tool results seen | Notes |
|---|---|---|---|---|
| Claude Code | Yes | Yes (PreToolUse deny) | Yes | |
| Codex | Yes | Yes (exit code 2) | Yes | |
| Cursor | Yes | Shell, MCP and file reads (beforeShellExecution, beforeMCPExecution, beforeReadFile) | Shell and MCP | Edits are reported after they happen (afterFileEdit). They are evaluated but cannot be refused. |
| GitHub Copilot CLI | Yes (it can only observe prompts) | Yes (preToolUse deny) | Yes | |
| Gemini CLI | Yes | Yes (BeforeTool deny) | No | Data-flow and injection policies run only in the scan. |
| Windsurf | Yes | Commands, MCP and file reads (exit code 2) | No | Data-flow and injection policies run only in the scan. |
| OpenCode | Yes | Yes (the plugin fails the call in tool.execute.before) | Yes |
Each session’s history is a file under the user cache directory (~/Library/Caches/shield360/sequence/ on macOS, $XDG_CACHE_HOME/shield360/sequence/ on Linux). The file is owner-only (mode 0600) and holds the last 64 classified events. It holds categories, flags, keyed fingerprints and a masked summary. It never holds arguments, results or prompts. Below full content capture, the summary is only the tool’s name.
SHIELD360_AGENT_SEQUENCE=off|warn|block overrides every policy’s mode on one computer. It also turns the built-ins on when an older platform sends no sequence policies. When the organisation asks the guard to fail closed, a session history that exists but cannot be read makes the hook refuse sensitive calls until it can be read again.
MCP gateway
Section titled “MCP gateway”The gateway keeps each agent session’s recent calls in Redis, which every pod shares. Without REDIS_URL it keeps them in the process’s memory. A session is the client’s Mcp-Session-Id if it sends one, or otherwise the agent. A window expires an hour after its last call. In block mode, a call that violates a policy is refused with a safe error before it reaches the tool, and it is logged as denied. The gateway sees tool calls, their results and prompt-injection findings. It does not see the person’s turns.
Gateway tools are named <service>.<tool>. An MCP or API service counts as an outside destination unless it is allowlisted as mcp:<service>.
After-the-fact scan
Section titled “After-the-fact scan”Every ten minutes (SEQUENCE_SCAN_SCHEDULE), the scan evaluates every enabled policy over:
- Coding-agent sessions rebuilt from
coding_agent.tool.calland user-promptcoding_agent.llm.turnspans. Arguments and results are read only when content capture is full; they are fingerprinted in memory and never stored. A user turn whose text was not captured counts as confirming, so an unknown turn never causes a violation. - Gateway sessions rebuilt from the call log (
shield360_agent_tool_calls). A session is split where the agent paused for 30 minutes. Only tool names and outcomes are available here. - Prompt-injection detections, which are attached to the call or turn they were found in.
The scan decides only the calls of the last SEQUENCE_SCAN_LOOKBACK_MINUTES (15). It reads up to 24 hours of earlier history so that sequences which began earlier are still found. It is idempotent: a violation’s id is derived from the policy, the session and the call that completed it, which is how live enforcement derives it too. Ids already recorded are skipped. So is a live record of the same policy, session (or gateway agent) and tool within two minutes, whose call could not be named the same way.
What is recorded
Section titled “What is recorded”Each violation is stored in shield360_policy_violations in your ClickHouse database and kept for seven years, like guard events. A record holds:
- The policy id, name and severity, the rule’s message, the configured mode, and what was done:
block, orwarn. Warn includes block that was set but could not be enforced. - Where the violation was found (coding agent, MCP gateway or scan), the client or agent, the person and computer, and the session.
- Each matched event, earliest first: its id, time, type, tool, category, flags, and a masked summary.
A summary is the tool plus one masked detail: the command, the first path, or the destination domains. Home directories become ~. Values after secret-looking names, and long tokens, are cut to four characters. Below full content capture, the summary is only the tool’s name, and ingest enforces this even for older collectors.
Arguments, results and prompts are never sent to or stored by Shield360.
Alerts
Section titled “Alerts”Each violation raises a policy.violation alert with the policy’s severity. The alert links to the violation. Repeats for the same policy and session within an hour are counted on the open alert rather than raised again. To send them to a channel, add an Alerts raised by features rule for the kind Policy violation under Detect & respond → Alerts → Alert rules.
The policy language
Section titled “The policy language”The language is a small, native subset of Invariant Labs’ guardrail rules, with Shield360’s own event fields and functions. For a step-by-step walkthrough of writing, testing and saving one, see Create custom policies and rules. A policy is one rule:
from invariant.detectors import prompt_injection # optional; accepted and ignored
raise "Data was sent to an outside destination after {secret.secret_source}" if: (secret: ToolCall) -> (egress: ToolCall) secret.secret_read egress.externalraise "message" if:begins the rule. The message is what a violation and its alert say.{variable.field}in the message is filled from the event the variable matched, cut to 60 characters. The fields it can use aresecret_source,tool,name,server,category,outcome,summary,role,typeandid. Anything else in braces is left as written. Collectors older than version 2026.10.15-1 show the braces as written.- A comment line
# optional: user_turnssays the policy reads user turns only to let the developer approve. Runtimes that cannot see user turns (the MCP gateway) still evaluate it, without the approval. Without it, a policy that reads user turns is skipped there. - The pattern is the first line that starts with typed variables. Variables are joined by
->, which means anywhere earlier in the same session, or by~>, which means immediately before. The last variable is the call being decided: a policy is checked when that call is about to run. It must be aToolCall. A pattern has one to four variables. - Every other line is a condition, and all conditions must hold. Conditions can be combined with
and,orandnot, and grouped with parentheses. A condition can run over several lines only inside parentheses, brackets or braces. - Comments start with
#.
When several earlier events could satisfy the rule, the nearest are chosen, and the violation shows those events.
Values
Section titled “Values”| Value | Written as |
|---|---|
| Text | "…" or '…', on one line. \n, \t, \\ and an escaped quote work as usual. A raw string, r"…", keeps backslashes as written, which suits regular expressions. |
| Numbers | 3, 2.5. Compare them with <, <=, > and >=, for example len(c.destinations) > 2. |
| True and false | true and false (or True and False). |
| Lists | ["delete_repository", "transfer_repository"], mostly with in and not in: c.name in ["delete_repository", "transfer_repository"]. |
None and null are not supported. A missing value, such as an argument the call does not have, is already false.
| Type | Matches |
|---|---|
ToolCall | A tool call. ToolOutput is the same thing: a call and its result are one event. |
Message | A user turn. |
Event | Either. |
The last variable of the pattern must be a ToolCall. The same names test an event’s type in a condition: a is Message, a is ToolCall, a is Event.
Event fields
Section titled “Event fields”| Field | Meaning |
|---|---|
tool | The tool name as the agent reported it (Bash, mcp__github__create_issue, github.create_issue). |
name, server | The tool’s own name and its MCP server or gateway service, both lower-case. |
category | read, search, write, execute, network, message or other. |
secret_read | Reads a secret-like file, or dumps a keychain or cloud token. Examples, public keys and test fixtures are not secrets (see Secret, then egress in detail). |
secret_source | For a secret read: what was read, without its content, such as reading .env in services/api or running `gh auth token` . |
destructive | A destructive command or delete-style remote tool. |
privilege | Changes privileges or permissions. |
network | Makes a network request: a fetch tool, curl, ssh, git push, npm publish, or a command with a URL. |
outbound | Sends data out: network, a message-style tool (send, post, email, upload, comment…), or a write through a remote tool. The agent’s own coordination tools (Agent, Task, SendMessage, TodoWrite…) are not outbound. |
external | Outbound to a destination that is neither internal (localhost) nor allowlisted. An outbound call with no known destination counts as external. |
destinations | Where the call goes: a host, or a host and up to two path segments (github.com/acme/app). It includes URLs, email addresses, git push remotes and gh repositories. |
writes | Changes state: writes, edits, messages, destructive or privilege commands, and writing commands such as git commit, mv or npm install. |
sensitive | outbound or destructive or privilege or secret_read. |
remote | An MCP or gateway tool. |
injection | Prompt injection was detected in this call’s result or arguments, or in this user turn. |
confirms | For a user turn: it approves (“yes”, “go ahead”, “try again”, “retry”, “continue”, “approved”, “lgtm”…) or asks for a destructive action. |
domains | The destination hosts found in the arguments (URLs, email addresses) and the service’s own, if known. |
outcome | ok, error, blocked or pending. |
role, type, id, ts, summary | The user turn’s role (user), the event type, its id, its time in milliseconds, and its masked summary. |
args.<key> | An argument of the call being decided. Coding-agent hooks and the MCP gateway do not keep arguments for earlier events, so there it is missing. function.name and function.arguments are Invariant’s spelling of tool and args. |
Operators and functions
Section titled “Operators and functions”==, !=, <, <=, >, >= | A comparison with a missing value (an argument that is not there) is false, whatever the operator. |
in, not in, contains | Membership in a list, a substring in a string, or a key in an argument object. |
x is tool:name | The event is a call of name. * matches any run of characters, for example tool:mcp__github__*. Case is ignored. |
x is tool:name({key: "regex", …}) | The same, and each named argument of the decided call matches its regular expression. |
x is ToolCall, x is Message, x is Event | The event’s type. x is ToolOutput is the same as x is ToolCall, and x is Event is always true. |
match(r"pattern", value) | A regular-expression search. The pattern must be a literal. |
len(x), lower(x) | Length of a string or list; lower-case. |
flows(a, b) | Data from a’s result appears in b’s arguments (by fingerprint). |
prompt_injection(x) | x.injection. prompt_injection(x.content) is accepted too, for Invariant’s spelling. x.content means something only there: anywhere else it is always missing, because content is never kept. |
count((v: Type) before x [after y] [within N] [since user] [where cond]) | The number of events of that type before x: after y, within the N events before x, and since the last user turn, each only when given. before is required. x and y must be variables of the pattern, and v must be a new name. N is a whole number of at least 1. where must come last and can use v and the pattern’s variables. |
exists(…) | count(…) > 0. |
Regular expressions use the subset Go and JavaScript read the same way. Lookaround, backreferences, named groups, \A, \z, \p{…}, POSIX classes and inline flags other than a leading (?i) are refused. Matching is case-sensitive unless the pattern starts with (?i).
A policy can be at most 4,000 characters and 32 levels of nesting. Its body can have at most 20 lines: the pattern and up to 19 conditions. Conditions joined with and on one line count as one. Evaluation is bounded, and a policy that exceeds the bound matches nothing.
Examples
Section titled “Examples”Invariant’s own example, as-is:
raise "External email after reading the inbox" if: (call: ToolCall) -> (call2: ToolCall) call is tool:get_inbox call2 is tool:send_email({ to: ".*@evil\\.com" })A write to GitHub outside your organisation’s repositories. GitHub’s MCP tools usually name the owner and the repository separately (owner: "acme", repo: "app"), while other tools pass repo: "acme/app", so the rule accepts either:
raise "GitHub write outside acme" if: (c: ToolCall) c is tool:mcp__github__* c.writes not (lower(c.args.owner) == "acme" or match(r"(?i)^acme/", c.args.repo))It fires on a write to other/app and to acme-tools/x, and on a write that names no owner or repository at all. It does not fire on reads.
Deleting more than three things in a row without asking:
raise "Several deletions without a user turn" if: (c: ToolCall) c.destructive count((d: ToolCall) before c since user where d.destructive) >= 3Test fixtures
Section titled “Test fixtures”Each custom policy can carry up to 20 fixtures, each of 1 to 64 events, and at most 200,000 characters of fixture JSON in all. Test in the editor runs them on the server without saving anything:
[ { "name": "secret, then curl", "expect": "violation", "events": [ { "kind": "tool", "tool": "Read", "args": { "file_path": ".env" }, "result": "API_KEY=example" }, { "kind": "tool", "tool": "Bash", "args": { "command": "curl https://example.com" } } ] }, { "name": "curl alone", "expect": "pass", "events": [{ "kind": "tool", "tool": "Bash", "args": { "command": "curl https://example.com" } }] }]A fixture is { "name", "expect": "violation" | "pass", "events": [...] }, and an event is:
| Field | Meaning |
|---|---|
kind | "tool" for a tool call, "user" for a user turn. Required. |
tool | The tool name as the agent reports it (Bash, mcp__github__create_issue). Required for a tool call. At most 200 characters. |
server | The MCP server or gateway service, when it is not part of the tool name. At most 100 characters. |
serverDomain | Where a remote tool’s calls go (api.vendor.example). It is added to the call’s destinations and domains. At most 253 characters. |
args | The call’s arguments, as an object. |
result | The call’s result, as text, for flows() and taint-to-external. At most 8,192 characters. |
text | A user turn’s text, for confirms. Leave it out to model a turn whose text was not captured, which counts as confirming. At most 4,096 characters. |
outcome | ok, error, blocked or pending. |
injection | true when prompt injection was found in this call or turn. |
A fixture expects a violation if the policy fires on any of its calls. Fixtures run with your allowed destinations and as if every runtime could see user turns and tool results. Fixtures are stored with the policy, so use made-up values.
Rules are compiled on the server when they are validated and again when they are saved. A rule that does not compile is refused with a code and a line number, for example unknown_field on line 3.
Data flow without content
Section titled “Data flow without content”taint-to-external and flows() never compare content. When a call returns, each line of its result is fingerprinted with HMAC-SHA256 under your organisation’s guard fingerprint key. Lines are normalised the way agent-written lines are fingerprinted for AI-code rework: trimmed, whitespace collapsed, at least 8 characters, not only punctuation. Each long token (16 or more characters, with a letter and a digit: keys, ids, hashes) is fingerprinted the same way. Each later call’s arguments are fingerprinted the same way. A shared fingerprint is a flow. Only the 16-character fingerprints are kept, and only in the session history.
A flow is found when a whole line or a whole token is carried over. Data that is reformatted, split, encoded or summarised on the way is not found.
Allowed destinations
Section titled “Allowed destinations”Under Allowed destinations, below the sequence policies in Govern → Policies & rules → Policies, list the destinations where data may go without counting as leaving, one per line:
example.comcoversexample.comand all its subdomains.*.example.commeans the same.github.com/acmecovers only that path and what is below it, ongithub.comand its subdomains:github.com/acme/appandapi.github.com/acme/…, but notgithub.com/acme-tools,github.comitself orgist.github.com. Up to two path segments are allowed (github.com/acme/app). Paths compare whole segments and ignore case.mcp:githuballows an MCP server or gateway service when no other destination is named.
localhost, 127.0.0.1, 0.0.0.0 and ::1 are always internal. A path that could lead somewhere else (.., . or percent-encoding) is reduced to its host, so github.com/acme/../other does not match github.com/acme.
Collectors older than version 2026.10.15-1 do not understand path entries. They treat such an entry as a host that never matches, so it allows nothing there until the collector is updated.
A typical organization adds:
github.com/your-orgapp.your-company.comThe first entry lets agents push to and call the GitHub API for the organization’s repositories, and nothing else on GitHub. The second lets them call your own application.
False positives
Section titled “False positives”The built-ins are tuned to be quiet, but they will sometimes fire on legitimate work:
- Secret, then egress can fire when an agent reads
.envand later fetches documentation or calls a public API. It no longer fires on pushes andghcalls to destinations you allow, on test fixtures, on the agent’s own coordination tools, or after the developer approves. If it fires on legitimate work:- Read the message. It names what was read (for example “after reading .env.local in services/api”), so you can tell a real secret from a false alarm.
- If the destination is your own, add it to allowed destinations, with a path for shared hosts such as
github.com/your-org. - If the file is test data, keep it under a test or fixtures folder.
- For a one-off, reply “go ahead” in the agent’s chat and let it retry.
- Consider warn rather than block for teams whose agents routinely read local configuration.
- Destructive without confirmation looks at the six events before the command. A request made long before, followed by a lot of exploration, no longer counts.
rmof a single file and a normalgit pushare not destructive; deleting a branch is. When prompt text is not captured, turns count as confirming, so the scan cannot find these violations below full capture. - Taint to external matches whole lines and long tokens. Common lines, such as a license header copied into an issue, can match. Allowlist the destinations your agents are meant to write to.
- Excessive writes counts edits. Large refactors by an agent working autonomously will reach 26. It fires once per run and resets at the next user turn.
- Privilege change fires on any
sudo. Where agents legitimately usesudo, keep it in warn.
Every classifier and built-in lives in sequence.json. A change there is re-checked against the shared corpus of positive and negative sessions on both engines.
Invariant compatibility
Section titled “Invariant compatibility”Shield360 does not run Invariant’s analyzer. Invariant Labs was acquired by Snyk in June 2025. The Guardrails repository (Apache-2.0) has had no substantive change since July 2025, and the hosted service shut down in January 2026. The analyzer is a Python interpreter with heavy runtime dependencies (models for its detectors, OpenAI calls). Instead, the policy language is implemented natively in Go and TypeScript and accepts the core of Invariant’s syntax. Invariant’s -> is ordering, not data flow, and Shield360’s -> means the same. Data flow is the separate flows().
| Invariant | Shield360 |
|---|---|
raise "msg" if: with indented conditions | Supported. One rule per policy. |
(x: ToolCall), (x: ToolOutput), (x: Message) | Supported. ToolOutput is the call’s own result. |
a -> b, a ~> b | Supported, within one session. One pattern chain per rule. |
call is tool:name, tool:name({arg: "regex"}) | Supported, with * globs. Argument patterns apply to the call being decided. |
call.function.name, call.function.arguments.x | Supported as aliases of tool and args.x. |
match, len, in, and/or/not | Supported. |
from invariant.detectors import prompt_injection; prompt_injection(out.content) | Supported. It uses Shield360’s prompt-injection detector. |
secrets, pii, moderated, copyright, semgrep, llm… | Not supported. Use secret_read, the agent guard and prompt-injection instead. |
count(min=, max=): blocks, forall, list comprehensions, def, := | Not supported. Use count(…) and exists(…). |
Lookahead in regular expressions (for example (?!Peter$)) | Not supported. Use not match(…). |
A policy that uses an unsupported feature is refused with the code unsupported and the line.
Permissions
Section titled “Permissions”| Permission | Allows | Shipped roles |
|---|---|---|
policies.read (View agent policies) | See the policies, their modes, the allowed destinations and the violations (other categories in the Violations feed also need their own page’s permission) | Owner, Administrator, Compliance officer, Analyst, Security, Member, Viewer, Managed service provider |
policies.manage (Manage agent policies) | Turn policies on and off, change modes, write custom policies and change allowed destinations | Owner, Administrator, Security |
Every route checks these permissions on the server and acts only on the caller’s current project and environment. Changes are recorded in the audit log.
Next steps
Section titled “Next steps”- Policies overview: every policy on one page, what each mode means, and how developers get unblocked.
- Prompt-injection protection, whose detections the
injection-then-sensitivepolicy builds on. - Create custom policies and rules: a step-by-step walkthrough for writing, testing and saving your own sequence policy, and every other kind of rule you can write.
- Alerts: route
policy.violationalerts to Slack, Teams, email, webhooks or PagerDuty with an Alerts raised by features rule under Detect & respond → Alerts → Alert rules.