# OpenRouter Armor: who actually stops a flagged tool call?

> OpenRouter Armor can withhold tool calls, but Messages compatibility leaves enforcement to the client. Read the boundaries around flagged, unavailable, skipped, and audit.

- Published: Oct 19, 2026
- Author: PiRouter Team, Engineering
- Tags: security, agents, gateway
- Canonical: https://pirouter.ai/blog/armor-tool-call-enforcement-boundaries

---
OpenRouter's Armor beta evaluates proposed agent tool calls against safety rules, but a safety verdict does not always stop execution by itself. In its Messages API compatibility mode, a call marked `flagged` remains a `tool_use` in the response: the client must refuse to run it. Other output modes withhold the executable call, so the distinction is not whether Armor can block anything—it is where your particular integration places the execution gate.

That boundary matters more than the name of the classifier behind it. A useful security review starts with a concrete question: after the model proposes an operation, which component can actually prevent it from reaching the tool?

This is a reading of the beta documentation, not a hands-on security evaluation. The behavior below is documented; its effectiveness against adversarial inputs has not been independently established by the sources reviewed here.

## Armor reviews intent; the executor still owns capability

Imagine a repository-maintenance agent that can inspect files and run tests, while publishing changes requires separate approval. This is a hypothetical workflow, not a customer incident. The agent needs enough freedom to finish ordinary maintenance without turning every command into another approval dialog, but that freedom must not quietly include publishing.

That is the tension behind this week's release. [GIGAZINE's Armor launch report](https://gigazine.net/news/20261016-openrouter-armor/) describes the choice between repeatedly asking people to approve commands and allowing automation to proceed without those checks. Armor inserts a decision model between the generating model and the application. The [official Armor documentation](https://openrouter.ai/docs/guides/features/plugins/armor) says it evaluates the proposed call, the rules, and the conversation before releasing or blocking the call.

The rules are not all equivalent. A `hard_deny` describes an operation that must never happen; once it matches, neither an allow rule nor a user's request clears it. A `soft_deny` describes something usually prohibited but sometimes legitimate. An allow rule, or the user requesting that exact operation on that exact target, can clear a soft-deny match. An `allow` rule does not independently block anything.

There is an important qualifier in “once it matches.” The decision model estimates whether a call matches prose rules. A hard-deny rule has final precedence after a match; its existence is not a mathematical guarantee that every prohibited operation will be recognized. OpenRouter explicitly acknowledges both false positives and misses.

The separate `environment` field supplies facts, not additional rules: the trusted repository, current branch, deployment target, or account in use. In the hypothetical maintenance workflow, stale environment facts could make a sensible publishing policy less precise. Keeping those facts current is part of maintaining the integration, not merely polishing the prompt.

A classifier judges the proposed action. The tool executor holds the ability to perform it. The next question is how those two exchange a refusal.

## A flagged Messages tool_use is still a tool call

The official documentation describes three representations for blocked calls. They are different execution contracts, not interchangeable ways to display an alert.

| Output mode | What happens to a blocked call | Where refusal is enforced |
|---|---|---|
| `text` on Chat Completions and Responses | The executable call is removed and replaced by a text marker | Armor withholds the call from the application's ordinary tool-call path |
| `custom_item` on Responses | The function call is replaced by an `openrouter:armor_blocked` item | Armor withholds the function call; the client must handle the replacement item correctly |
| `claude_code_compatibility` on Messages | The `tool_use` remains, accompanied by a `flagged` verdict | The client must decline execution |

![Official Armor documentation comparing text, custom item and Messages compatibility output modes](/blog/images/armor-tool-call-enforcement-boundaries-page-modes.png "The official output-mode table distinguishes withholding a call from returning a tool_use with a verdict. This is documentation, not a response captured in our own test. Source: [OpenRouter Armor documentation](https://openrouter.ai/docs/guides/features/plugins/armor)")

Changing APIs can also change the default rule set: `use_cc_style_default_rules` defaults to true on Messages and false on Chat Completions and Responses. Custom rules cannot remove its built-in rules when enabled, so check that setting as well as the response format.

The compatibility row is the critical one. The response can still end with `stop_reason: "tool_use"`. That tells the client what kind of response it received, not whether the proposed action has permission to run. The decision appears separately in `safeguard_results`, keyed by the tool-call identifier.

OpenRouter's documentation is unusually direct about the consequence: a Messages client that ignores `safeguard_results` will run a flagged `tool_use`. A compatible client must connect the verdict to the matching call, decline it, and return an error tool result so the model can continue with the refusal in its history.

This is why confirming that the response contains the word `flagged` is an incomplete acceptance test. The test must also establish that the execution function was never invoked. An explanatory assistant message is not evidence that the application refrained from performing an action.

The docs offer an alternative for developers who do not control their Messages client's implementation: explicitly request `output_mode: "text"` so OpenRouter withholds the call. That instruction appears in the compatibility section, although the summary table lists text mode under Chat Completions and Responses. Treat it as an integration behavior to verify with the intended client, rather than silently assuming every mode/API combination works; the same page says unsupported combinations return a 400.

There is also a routing distinction inside the compatibility path. According to OpenRouter, when a request reaches an Anthropic endpoint supporting the relevant beta, Anthropic's classifier evaluates the call and its verdict is passed through. Armor evaluates on other endpoints. “The same client contract” therefore does not mean “the same classifier handled every request.”

## Unavailable, skipped, and audit are not approval states

A correctly handled `flagged` result is only one branch. The less obvious branches are the ones where no blocking judgment was made—or blocking was deliberately disabled.

**`unavailable` means the decision model could not answer.** The official examples include timeouts, errors, and requests too large for the decision model. Outside compatibility mode, `on_error` determines the response: `block`, the default, blocks the call and tells the model it could not be evaluated; `log` releases it.

Under `claude_code_compatibility`, however, an unavailable call is always released with that status visible. The documentation describes Claude Code falling back to its own local auto-mode evaluation. That fallback belongs to the documented Claude Code workflow. A custom Messages client does not acquire an equivalent local evaluator just because it understands the response shape.

For the hypothetical maintenance agent, this creates a specific ownership decision. When remote classification cannot answer, the local application needs an explicit policy for what happens next. Requiring approval or declining a sensitive operation are possible application choices—not promises made by the `unavailable` status.

**`skipped` means the call was not evaluated.** By default, `evaluate_native_tools` is false. Provider-native server tools can run on the provider's side and be reported as skipped. A skipped result is not a positive assessment, nor evidence that the operation waited for the application's executor.

Setting `evaluate_native_tools` to true forces those tools out of the native path so Armor can evaluate them, according to the options reference. That is a change to the execution path, not simply an extra log field. OpenRouter's own server tools are another case: the docs say a blocked server tool is refused within the request. An inventory must distinguish those locations before claiming complete coverage.

**Audit mode intentionally releases every call**, including flagged calls. It uses the same evaluation and records the verdicts, but it is an observation mode, not an enforcement rehearsal with real blocking. It also cannot be combined with `claude_code_compatibility`.

![Official Armor documentation describing unavailable verdicts and data-restricted requests](/blog/images/armor-tool-call-enforcement-boundaries-page-unavailable.png "The unavailable branch differs between ordinary plugin behavior and Messages compatibility. The page also describes the data-restriction exception; it is not evidence that a custom client has a local fallback. Source: [OpenRouter Armor documentation](https://openrouter.ai/docs/guides/features/plugins/armor)")

Those distinctions also affect streaming. The official documentation places `safeguard_results` at the end of the stream, with API-specific event locations. For a client whose execution decision depends on that field, the engineering implication is to wait for the final applicable verdict before dispatching a tool. Receiving arguments earlier is not receiving permission earlier.

## Data restrictions change the failure path

Availability is not only a timeout problem. The official page says Armor evaluation is not yet supported for requests under HIPAA, Zero Data Retention, or a data-region restriction. A request explicitly enabling the plugin in those contexts is rejected with a 403.

The documented Claude Code behavior is different: its request is not rejected; the call is released with an unavailable verdict, and Claude Code falls back to local auto-mode evaluation. That is neither remote Armor coverage nor a blanket statement that custom agents work under those restrictions. Teams with those requirements need to determine which path their integration actually takes before adopting the feature.

Pricing and performance deserve the same source discipline. Evaluation is not billed during the beta; that statement does not make the underlying model's inference free or establish a permanent price. OpenRouter publishes latency figures for its compatible rule set, but those are vendor-reported measurements, not an independent benchmark of a reader's workflow.

[Explainx's claimed-versus-verified Armor analysis](https://explainx.ai/blog/openrouter-armor-beta-tool-call-safety-classifier-jev-claude-code-auto-mode-claimed-vs-verified-2026) usefully separates documented behavior from unverified comparative claims. Its statement that the documentation publishes neither latency nor cost information no longer matches the official page reviewed here, so it is not the basis for those details. Its broader caution still applies: documentation of a classifier is not proof of superior security accuracy.

## Test the execution boundary, not just the verdict

The useful next step is not a dangerous live demonstration. It is a controlled integration check using inert tools that record an attempted invocation without making external changes. The following is a proposed acceptance approach, not a report of tests performed for this article.

Start with the actual client and API path intended for deployment. Give a harmless stub operation a policy that should flag it. In Messages compatibility mode, confirm both that the matching verdict is read and that the stub's execution counter stays at zero. A correct log entry beside an incremented execution counter is a failed boundary.

Then check the representation of refusals on other supported paths. A Responses `openrouter:armor_blocked` item is not an ordinary function call. The official contract says to echo it back unchanged in the next turn, without manufacturing a separate function-call output for it. A generic dispatcher needs to distinguish that item from work to execute, while preserving the history the model needs to understand the refusal.

Next, exercise a simulated unavailable result in the local integration. Confirm that it enters a distinct branch rather than being treated as `not_flagged`. Record the policy for that branch and its owner. If the intended fallback is human approval, verify the tool remains idle until approval arrives; if a local evaluator owns the decision, verify that handoff rather than assuming it exists.

Review native tools separately. A successful test of an application-defined tool says nothing about a provider-side operation that the application never executes. List which tools can be skipped, which are evaluated after leaving the native path, and which run inside OpenRouter's request. Coverage should name those paths rather than say “all tools” without qualification.

Finally, keep audit and enforce evidence separate. Audit can help expose policy mismatches on appropriately controlled traffic, but a flagged call executing in audit is expected. Evidence of blocking must come from the enforcing path. Because beta fields and response shapes may change, retain these checks as compatibility tests rather than a one-time screenshot of a successful verdict.

![Official Armor audit-mode explanation stating that flagged calls are still released](/blog/images/armor-tool-call-enforcement-boundaries-page-audit.png "Audit records verdicts without blocking calls. The screenshot illustrates that distinction, not a complete copyable JSON example or a security test performed for this article. Source: [OpenRouter Armor documentation](https://openrouter.ai/docs/guides/features/plugins/armor)")

The sources point toward a layered decision, not a replacement of every existing control. GIGAZINE explains the pressure to reduce repeated approval prompts; Explainx distinguishes classification from capability isolation; the official contract shows exactly where the client's responsibility remains. The engineering recommendation is to retain scoped tool credentials, appropriate isolation, and explicit approval for actions whose consequences justify it. A probabilistic policy judgment should not be the only thing standing between an agent and every capability available to its process.

For a team evaluating Armor today, the decisive acceptance question is simple: **when the result is flagged—or no judgment is available—which component prevents execution, and what evidence shows that it did?** Until that question has a concrete answer for the chosen API, mode, and tool path, the safety field is an observation, not a completed control.

---

## Sources

### gigazine.net

- [GIGAZINE's Armor launch report](https://gigazine.net/news/20261016-openrouter-armor/)

### openrouter.ai

- [official Armor documentation](https://openrouter.ai/docs/guides/features/plugins/armor)

### explainx.ai

- [Explainx's claimed-versus-verified Armor analysis](https://explainx.ai/blog/openrouter-armor-beta-tool-call-safety-classifier-jev-claude-code-auto-mode-claimed-vs-verified-2026)
