{"record":{"id":"9ad817574613f60e","repo":"BerriAI/litellm","slug":"content-blocked-context-label-argument-matched","errorCode":null,"errorMessage":"Content blocked: {context_label} argument matched a masking rule on a non-rewritable field","messagePattern":"Content blocked: (.+?) argument matched a masking rule on a non-rewritable field","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py","lineNumber":1760,"sourceCode":"            start_time=start_time.timestamp(),\n            end_time=datetime.now().timestamp(),\n            duration=(datetime.now() - start_time).total_seconds(),\n            masked_entity_count=masked_entity_count,\n            tracing_detail=GuardrailTracingDetail(**tracing_kw),\n        )\n\n    @staticmethod\n    def _get_mcp_tool_name(request_data: dict) -> str | None:\n        raw_name: Final[object] = request_data.get(\"mcp_tool_name\")\n        if isinstance(raw_name, str) and raw_name:\n            return raw_name\n        return None\n\n    def _assert_argument_label_clean(\n        self, text: str, detections: list[ContentFilterDetection], context_label: str\n    ) -> None:\n        if self._filter_single_text(text, detections=detections) != text:\n            raise HTTPException(\n                status_code=400,\n                detail={\n                    \"error\": (\n                        f\"Content blocked: {context_label} argument matched a masking rule on a non-rewritable field\"\n                    )\n                },\n            )\n\n    def _filter_argument_value(\n        self,\n        value: object,\n        detections: list[ContentFilterDetection],\n        context_label: str,\n        depth: int = 0,\n    ) -> object:\n        if depth > DEFAULT_MAX_RECURSE_DEPTH:\n            raise HTTPException(\n                status_code=400,","sourceCodeStart":1742,"sourceCodeEnd":1778,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py#L1742-L1778","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rename the offending dict key so it no longer matches the masking pattern - keys are validated, never masked","Move sensitive-looking data into string values, which the masker rewrites in place instead of blocking","Adjust the guardrail's masking rules/categories so they do not apply to MCP argument labels","Remove PII-shaped literals (card/SSN-like numbers, emails) from argument keys and numeric fields"],"exampleFix":"# before - PII in a dict key: cannot be rewritten, request is blocked\ntool_arguments = {\"user@corp.com\": \"notify\"}\n\n# after - PII moved into a string value: gets masked, request proceeds\ntool_arguments = {\"contact\": \"user@corp.com\"}","handlingStrategy":"validation","validationCode":"def assert_mcp_args_labels_clean(args: dict, mask) -> None:  \n    \"\"\"Mirror of the guardrail check: keys and scalars must survive masking unchanged.\"\"\"  \n    def walk(node, depth=0):  \n        if depth > 100:  \n            raise ValueError(\"arguments too deep\")  \n        if isinstance(node, dict):  \n            for k, v in node.items():  \n                if isinstance(k, str) and mask(k) != k:  \n                    raise ValueError(f\"dict key matches a masking rule and cannot be rewritten: {k!r}\")  \n                walk(v, depth + 1)  \n        elif isinstance(node, (list, tuple)):  \n            for v in node:  \n                walk(v, depth + 1)  \n        elif isinstance(node, (int, float)) and not isinstance(node, bool):  \n            if mask(str(node)) != str(node):  \n                raise ValueError(f\"numeric value matches a masking rule: {node}\")  \n    walk(args)","typeGuard":"def is_safe_tool_arguments(args: object) -> bool:  \n    \"\"\"True when all dict keys are plain identifiers with no PII-shaped text.\"\"\"  \n    import re  \n    piiish = re.compile(r\"[\\w.+-]+@[\\w-]+\\.[\\w.]+|\\d[\\d\\s-]{11,}\")  \n    def keys_clean(node):  \n        if isinstance(node, dict):  \n            return all(isinstance(k, str) and not piiish.search(k) and keys_clean(v) for k, v in node.items())  \n        if isinstance(node, (list, tuple)):  \n            return all(keys_clean(v) for v in node)  \n        return True  \n    return keys_clean(args)","tryCatchPattern":null,"preventionTips":["Never put user-supplied text into dict keys of tool arguments - use fixed key names and put variable data in string values","Avoid PII-shaped test numbers (card-like digit strings) in argument payloads","Add a pre-send lint in your MCP client that regex-scans keys for emails, phones, and long digit runs"],"tags":["mcp","content-filter","pii-masking","guardrail","http-400"],"backgroundTag":"pii-masking-blocked","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}