BerriAI/litellm · error · HTTPException

Content blocked: {context_label} argument matched a masking

Error message

Content blocked: {context_label} argument matched a masking rule on a non-rewritable field

What it means

The content filter guardrail walks MCP tool-call arguments and masks text that matches masking rules. Dict keys and numeric scalars cannot be rewritten without breaking the tool schema, so the guardrail instead asserts they are clean: if _filter_single_text would change the text (it matches a masking rule), the whole request is blocked with HTTP 400. context_label identifies which argument scope (e.g. which tool's arguments) failed.

Source

Thrown at litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py:1760

            start_time=start_time.timestamp(),
            end_time=datetime.now().timestamp(),
            duration=(datetime.now() - start_time).total_seconds(),
            masked_entity_count=masked_entity_count,
            tracing_detail=GuardrailTracingDetail(**tracing_kw),
        )

    @staticmethod
    def _get_mcp_tool_name(request_data: dict) -> str | None:
        raw_name: Final[object] = request_data.get("mcp_tool_name")
        if isinstance(raw_name, str) and raw_name:
            return raw_name
        return None

    def _assert_argument_label_clean(
        self, text: str, detections: list[ContentFilterDetection], context_label: str
    ) -> None:
        if self._filter_single_text(text, detections=detections) != text:
            raise HTTPException(
                status_code=400,
                detail={
                    "error": (
                        f"Content blocked: {context_label} argument matched a masking rule on a non-rewritable field"
                    )
                },
            )

    def _filter_argument_value(
        self,
        value: object,
        detections: list[ContentFilterDetection],
        context_label: str,
        depth: int = 0,
    ) -> object:
        if depth > DEFAULT_MAX_RECURSE_DEPTH:
            raise HTTPException(
                status_code=400,

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Rename the offending dict key so it no longer matches the masking pattern - keys are validated, never masked
  2. Move sensitive-looking data into string values, which the masker rewrites in place instead of blocking
  3. Adjust the guardrail's masking rules/categories so they do not apply to MCP argument labels
  4. Remove PII-shaped literals (card/SSN-like numbers, emails) from argument keys and numeric fields

Example fix

# before - PII in a dict key: cannot be rewritten, request is blocked
tool_arguments = {"user@corp.com": "notify"}

# after - PII moved into a string value: gets masked, request proceeds
tool_arguments = {"contact": "user@corp.com"}
Defensive patterns

Strategy: validation

Validate before calling

def assert_mcp_args_labels_clean(args: dict, mask) -> None:  
    """Mirror of the guardrail check: keys and scalars must survive masking unchanged."""  
    def walk(node, depth=0):  
        if depth > 100:  
            raise ValueError("arguments too deep")  
        if isinstance(node, dict):  
            for k, v in node.items():  
                if isinstance(k, str) and mask(k) != k:  
                    raise ValueError(f"dict key matches a masking rule and cannot be rewritten: {k!r}")  
                walk(v, depth + 1)  
        elif isinstance(node, (list, tuple)):  
            for v in node:  
                walk(v, depth + 1)  
        elif isinstance(node, (int, float)) and not isinstance(node, bool):  
            if mask(str(node)) != str(node):  
                raise ValueError(f"numeric value matches a masking rule: {node}")  
    walk(args)

Type guard

def is_safe_tool_arguments(args: object) -> bool:  
    """True when all dict keys are plain identifiers with no PII-shaped text."""  
    import re  
    piiish = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+|\d[\d\s-]{11,}")  
    def keys_clean(node):  
        if isinstance(node, dict):  
            return all(isinstance(k, str) and not piiish.search(k) and keys_clean(v) for k, v in node.items())  
        if isinstance(node, (list, tuple)):  
            return all(keys_clean(v) for v in node)  
        return True  
    return keys_clean(args)

Prevention

When it happens

Trigger: An MCP tool call whose arguments dict contains a key, or a numeric value whose string form, matches a masking pattern - e.g. a key literally containing an email address, phone number, or card-shaped digits, or a test number like 4111111111111111 passed as an int argument.

Common situations: Dynamically built argument dicts where user-supplied text leaks into keys; human-readable field names that accidentally trip PII regexes; test fixtures using card/SSN-shaped numbers; serialization code that puts identifiers into dict keys instead of values.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/9ad817574613f60e. Report an issue: GitHub.