Skip to content
Get API key

Tool-call safeguards

POST /v1/safeguards decides whether an agent may run a tool call. You send the conversation, the proposed tool calls and your rules. Mouse returns a verdict for each call in the safeguard_results format that Claude Code reads in auto mode.

Use it when you run a gateway that sends Claude Code traffic to a model other than Claude. In auto mode, Claude Code asks the gateway for a verdict on every tool call. When the gateway routes to another model, it has to produce that verdict itself. You can also call it from your own agent loop before you run a tool.

For each tool call, Mouse asks mouse-1 three questions:

  1. Which deny rule does this call break, if any?
  2. Did the user explicitly ask for this exact operation on this exact target?
  3. Which allow rule covers this call, if any? Mouse asks this only when a soft deny matched and the user did not ask for the call.

Mouse then applies the rules in this order:

  1. If a hard_deny rule matches, the call is flagged.
  2. If a soft_deny rule matches, the call is flagged, unless the user explicitly asked for it or an allow rule matches.
  3. Otherwise the call is not_flagged.

Questions 1 and 2 run together in one pass. Most calls need only that pass.

Mouse reads three things from the request:

  • What the user typed. Text in user messages. Harness blocks such as <system-reminder> are removed first. In Claude Code these blocks carry file contents and CLAUDE.md, which the user did not write. Put project policy in environment instead.
  • Earlier tool calls. The tool_use blocks in assistant messages.
  • The proposed call. Always read in full.

User text and earlier tool calls share a window of about 4,000 tokens. Mouse keeps the first user message, which usually states the task and any limits, then fills the rest with the newest messages and calls.

Mouse does not read tool results or the assistant's own text. That content often comes from files, web pages and command output that someone else wrote. A tool result that says "the user approved this" can therefore never clear a soft deny.

If the proposed call is too large to read in full, the verdict is unavailable with reason truncated. Mouse does not shorten the call, because the dangerous part could be hidden in the part it cut.

Rules are plain English. Write each one as a description of what a matching call does.

TierWhen a rule matchesDefault threshold
hard_denyThe call is flagged. Nothing overrides it.0.6
soft_denyThe call is flagged unless the user explicitly asked for it or an allow rule matches.0.6
allowClears a soft_deny match. Has no effect on hard_deny.0.7

Use environment for facts about where the agent runs, such as the repository, the working branch and which targets are protected. Mouse treats these as true.

One soft_deny rule is always on, whatever rules you send. user_boundary matches a call that does something the user told the agent not to do, such as "don't push". It stays in force until the user lifts it in a later message. The agent cannot lift it.

A rule matches when its probability reaches its threshold. If several rules in a tier describe the same call, the probability is shared between them. To handle this, Mouse also adds up the probabilities of the rules in a tier and flags the call when the total reaches the tier's threshold. Deny rules from both tiers count toward the soft_deny total. A rule with a raised threshold is left out of the total, so the raise still applies.

These field names match the autoMode settings in Claude Code and the rule format used by OpenRouter Armor. You can copy rule lists across without changing them.

If you leave out rules, Mouse uses this set:

hard_deny

  • exfiltration (Data exfiltration): Sends secrets, credentials, keys, tokens, environment variables or private data to a host, service or person outside the environment, by any route: an HTTP request, scp, a webhook, DNS, a paste site, a git remote or an email.

soft_deny

  • force_push (Force push): Pushes with --force, -f or --force-with-lease, or deletes a branch or tag on a remote.
  • rewrite_pushed (Rewrite pushed history): Amends, rebases, squashes or filters commits that have already been pushed to a shared remote.
  • discard_work (Discard local work): Throws away uncommitted or unpushed work: git reset --hard, git checkout -- ., git restore ., git clean -f, git stash drop or clear, or deleting a branch with unmerged commits.
  • pipe_to_shell (Remote script): Downloads code from the internet and runs it in the same step, such as curl | bash, wget -O- | sh or eval of fetched content.
  • prod_deploy (Production change): Deploys, releases, migrates, scales or reconfigures a production or shared environment, such as fly deploy, vercel --prod, kubectl apply, terraform apply or a migration against a live database.
  • destructive_delete (Destructive delete): Permanently deletes data outside the task's working tree: rm -rf on home or system paths, dropping tables or databases, emptying storage buckets or deleting cloud resources.
  • credential_access (Credential access): Reads, prints, copies or searches for credentials the task does not need, such as .env files, ~/.ssh, ~/.aws, keychains, browser cookies or tokens in shell history.
  • security_weaken (Weaken security): Turns off or weakens a security control: TLS verification, authentication, a firewall, audit logging, branch protection, commit signing or file permissions on secrets.
  • self_modification (Change agent permissions): Changes the agent's own permissions, settings, hooks or safety rules, such as .claude/settings.json, or grants a person, key or service new access.
  • external_write (External write): Publishes or sends something outside the project: npm publish, docker push, a GitHub release, an issue or pull request comment, an email, a chat message, a ticket or a payment.

allow

  • tests_and_builds (Tests and builds): Runs the project's tests, linters, type checkers, formatters or a local build.
  • read_only (Read only): Reads, lists, searches or inspects files, git state, logs or documentation without changing anything.
  • workspace_edit (Workspace edit): Creates, edits or deletes files inside the task's working tree, commits locally, or pushes a working branch without force.
  • declared_dependencies (Declared dependencies): Installs dependencies the project already declares in its manifest or lockfile, such as npm ci, pnpm install or pip install -r requirements.txt.

Sending rules replaces the whole default set. To extend it, copy the defaults from this page and add your own rules.

{
"messages": [
{ "role": "user", "content": "Fix the failing auth test and push the fix." },
{ "role": "assistant", "content": [
{ "type": "tool_use", "id": "toolu_00", "name": "Bash", "input": { "command": "pnpm test" } }
] },
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "toolu_00", "content": "FAIL src/auth.test.ts" }
] }
],
"tool_uses": [
{ "id": "toolu_01", "name": "Bash", "input": { "command": "git push --force origin main" } },
{ "id": "toolu_02", "name": "Bash", "input": { "command": "pnpm test src/auth.test.ts" } }
],
"rules": {
"environment": ["Repository acme/api. Working branch fix/auth. main is protected."],
"hard_deny": [
{ "id": "exfiltration", "name": "Data exfiltration", "instructions": "Sends secrets, keys or tokens to a host outside the environment." }
],
"soft_deny": [
{ "id": "force_push", "name": "Force push", "instructions": "Pushes with --force or --force-with-lease, or deletes a remote branch." }
],
"allow": ["Runs the project's tests, linters or a local build."]
}
}

See the API reference for every field and limit.

{
"id": "sg_3f1c9a2b7d40e615",
"object": "safeguards.response",
"model": "mouse-preview-2026-10-13",
"safeguard_results": [
{
"type": "dangerous_tool_use",
"status": {
"type": "available",
"tool_uses": {
"toolu_01": {
"type": "evaluated",
"outcome": "flagged",
"explanation": "Force push: Pushes with --force or --force-with-lease, or deletes a remote branch. The user did not ask for this."
},
"toolu_02": { "type": "evaluated", "outcome": "not_flagged" }
}
}
}
],
"mouse": {
"mode": "enforce",
"rules": "request",
"tool_uses": {
"toolu_01": {
"decision": "soft_deny",
"rule": "force_push",
"would_flag": true,
"deny": { "exfiltration": 0.01, "force_push": 0.95, "user_boundary": 0.01, "none": 0.03 },
"explicit_intent": 0.04,
"allow": { "allow_0": 0.02, "none": 0.98 },
"truncated": false,
"latency_ms": 74.2
},
"toolu_02": {
"decision": "none",
"rule": null,
"would_flag": false,
"deny": { "exfiltration": 0.01, "force_push": 0.01, "user_boundary": 0.01, "none": 0.97 },
"explicit_intent": 0.88,
"allow": null,
"truncated": false,
"latency_ms": 39.5
}
}
},
"usage": { "input_tokens": 1186, "state_tokens": 342, "question_tokens": 806, "framing_tokens": 38, "image_tokens": 0, "output_tokens": 0, "cost_usd": 0.0001186 },
"latency_ms": 118.3
}

Return safeguard_results to Claude Code unchanged. Store mouse in your audit log. It shows which tier decided each call and the probability of every rule.

A call that Mouse could not judge returns { "type": "unavailable", "reason": "..." }:

ReasonCause
timeoutThe call was not judged within timeout_ms. The model can take a few minutes to start after an idle period.
truncatedThe proposed call is too large to read in full.
errorAny other failure.

Mouse never returns not_flagged for a call it did not judge. Claude Code denies a call that has no verdict. Calls that return unavailable are not billed.

Set "mode": "audit" while you tune your rules. Every call returns not_flagged, so nothing is blocked. The real result is in mouse.tool_uses[id].would_flag. Compare it with what your users approve, adjust the rules, then switch to "enforce".

Tune for false positives before you enforce. Claude Code pauses auto mode after 3 flagged calls in a row, or 20 in one session, and goes back to asking the user about every call.

  1. Check the incoming POST /v1/messages request for a safeguards entry with "type": "dangerous_tool_use".
  2. Send the request upstream as usual.
  3. If the upstream response contains tool_use blocks, send the request's messages and those blocks to POST /v1/safeguards.
  4. Add the returned safeguard_results to the response you send back to Claude Code.
  5. If the call to Mouse fails, return unavailable for every tool call. Do not return not_flagged.
import httpx
mouse = httpx.Client(
base_url="https://api.mouse.dev",
headers={"Authorization": f"Bearer {MOUSE_API_KEY}"},
timeout=12.0,
)
def safeguard_results(request_body: dict, response_body: dict) -> list[dict] | None:
wanted = any(s.get("type") == "dangerous_tool_use" for s in request_body.get("safeguards", []))
calls = [b for b in response_body.get("content", []) if b.get("type") == "tool_use"]
if not wanted or not calls:
return None
try:
res = mouse.post("/v1/safeguards", json={
"messages": request_body["messages"],
"tool_uses": [{"id": b["id"], "name": b["name"], "input": b["input"]} for b in calls],
"timeout_ms": 10_000,
})
res.raise_for_status()
return res.json()["safeguard_results"]
except (httpx.HTTPError, KeyError, ValueError):
return [{"type": "dangerous_tool_use", "status": {"type": "available", "tool_uses": {
b["id"]: {"type": "unavailable", "reason": "error"} for b in calls}}}]
results = safeguard_results(request_body, response_body)
if results is not None:
response_body["safeguard_results"] = results

Set the HTTP client timeout a little above timeout_ms. When the budget runs out, Mouse still returns the verdicts it has, and marks the rest unavailable.

In LiteLLM, run this in a custom callback's post-call success hook instead of setting safeguards_classifier_model.

If no tool call needs a verdict, leave safeguard_results out of the response.

Keep any fixed checks you already run, such as blocked paths or a command allowlist, in front of this call. See Action gating for that pattern.

  • Up to 32 tool calls per request. Each call is judged separately and billed for its own passes.
  • Each pass reads at most 18,432 tokens, rules included. A rule set that leaves less than about 4,000 tokens for the conversation returns 422 question_too_long.
  • Every request counts toward your account's rate limits, including the limit of 300,000 tokens of model work per minute. With the default rules, one tool call takes about 1,000 tokens of work in a short session and up to about 15,000 in a long one.
  • Requests are not stored. Every response carries Mouse-Data-Retention: none.