Summary
plugins/security-guidance/hooks/llm.py → _call_claude builds the Messages API request body with a top-level output_format field. The API now rejects it:
HTTP 400 invalid_request_error
output_format: This field is deprecated. Use 'output_config.format' instead.
See https://platform.claude.com/docs/en/build-with-claude/structured-outputs
Since this is the structured-output request the LLM-backed hooks rely on, every hook fire 400s → _call_claude returns None → the security guardrail silently fails open, while still consuming one API call per fire. The structured-outputs-2025-11-13 beta header is already sent in _build_auth_headers; only the request-body field name changed upstream.
Repro
Replay the exact request _call_claude builds (beta header structured-outputs-2025-11-13, model e.g. claude-opus-4-7, top-level output_format) against /v1/messages → the 400 above. Swapping the field to output_config.format makes it pass validation.
Affected versions
Reproduced on security-guidance@2.0.0 and present on main HEAD (plugins/security-guidance/hooks/llm.py:482).
Fix
Nest the schema under output_config.format. Gotcha: the adaptive-thinking branch already does payload["output_config"] = {"effort": "high"}, which would clobber the schema — so effort must be merged into the existing dict, not reassigned:
- "output_format": {
- "type": "json_schema",
- "schema": output_schema
- }
+ "output_config": {
+ "format": {
+ "type": "json_schema",
+ "schema": output_schema,
+ }
+ },
...
- payload["output_config"] = {"effort": "high"}
+ payload["output_config"]["effort"] = "high"
The two claude_agent_sdk ClaudeAgentOptions(output_format=...) call sites are a separate SDK contract and should be left unchanged.
I opened #2097 with this exact patch before realizing the repo only accepts internal PRs (auto-closed) — the diff there is ready to cherry-pick.
Summary
plugins/security-guidance/hooks/llm.py→_call_claudebuilds the Messages API request body with a top-leveloutput_formatfield. The API now rejects it:Since this is the structured-output request the LLM-backed hooks rely on, every hook fire 400s →
_call_claudereturnsNone→ the security guardrail silently fails open, while still consuming one API call per fire. Thestructured-outputs-2025-11-13beta header is already sent in_build_auth_headers; only the request-body field name changed upstream.Repro
Replay the exact request
_call_claudebuilds (beta headerstructured-outputs-2025-11-13, model e.g.claude-opus-4-7, top-leveloutput_format) against/v1/messages→ the 400 above. Swapping the field tooutput_config.formatmakes it pass validation.Affected versions
Reproduced on
security-guidance@2.0.0and present onmainHEAD (plugins/security-guidance/hooks/llm.py:482).Fix
Nest the schema under
output_config.format. Gotcha: the adaptive-thinking branch already doespayload["output_config"] = {"effort": "high"}, which would clobber the schema — soeffortmust be merged into the existing dict, not reassigned:The two
claude_agent_sdkClaudeAgentOptions(output_format=...)call sites are a separate SDK contract and should be left unchanged.I opened #2097 with this exact patch before realizing the repo only accepts internal PRs (auto-closed) — the diff there is ready to cherry-pick.