惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
人人都是产品经理
人人都是产品经理
WordPress大学
WordPress大学
The Cloudflare Blog
IT之家
IT之家
雷峰网
雷峰网
小众软件
小众软件
博客园 - 叶小钗
博客园 - 聂微东
爱范儿
爱范儿
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
V
Visual Studio Blog
博客园 - 【当耐特】
V
V2EX
博客园_首页
T
Tailwind CSS Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Block-Reason Headers: Make Your Security Proxy Tell You Why
Josh Waldrep · 2026-05-10 · via DEV Community

Josh Waldrep

When a security proxy blocks an agent's request, the agent sees a 4xx and has to guess what happened. Was the destination wrong? The body? A header? Did the proxy timeout? Did the proxy itself crash? Without context, every block looks the same and the agent burns its retry budget on a single attempt's worth of information.

X-Pipelock-Block-Reason is the header Pipelock emits on every block path so the agent knows. The vocabulary is small, the format is open-spec, and the impact on operator debugging is large. This post is about the design, the schema, and why making a security proxy explain itself is good for the security posture, not bad for it.

The problem the header solves

A coding agent runs a tool that fetches a URL, parses the response, and feeds the output back to the model. The fetch goes through Pipelock. Pipelock decides the response contains a prompt-injection pattern and returns 403 with no body.

The agent has no idea what happened. From the agent's perspective:

  • The host could be unreachable.
  • The proxy could be misconfigured.
  • The proxy could be down.
  • The destination could be returning 403 itself.
  • The agent's request could have failed scanning.
  • The agent's response could have failed scanning.

Each of these has a different correct response from the agent. "Host unreachable" might mean "try a different host." "Proxy misconfigured" might mean "tell the operator." "Scanning blocked the request" might mean "do not retry this exact body." Without a signal, the agent treats them all the same way: retry, hit the same block, retry again, eventually give up.

The operator's view is no better. The audit log records the block, but correlating an agent's confused retry sequence with the proxy's decision tree means cross-referencing two log streams by timestamp and request ID. For one block in a quiet period, fine. For a fleet generating thousands of requests an hour, painful.

A structured block reason on the response solves both sides. The agent knows what happened. The operator does not have to grep two logs to figure out what the agent saw.

This is the operator-facing half of enforcement. Politeness vs Enforcement explains how to make the kernel refuse bypasses; block-reason headers explain what the agent should do after the refusal.

The header schema

The full schema lives at docs/specs/block-reason-header.md in the Pipelock repo. The shape, in one paragraph:

X-Pipelock-Block-Reason: <reason> with companion headers for version, severity, retry, and the layer that fired:

X-Pipelock-Block-Reason: dlp_match
X-Pipelock-Block-Reason-Version: 1
X-Pipelock-Block-Reason-Severity: critical
X-Pipelock-Block-Reason-Retry: none
X-Pipelock-Block-Reason-Layer: dlp

Enter fullscreen mode Exit fullscreen mode

X-Pipelock-Block-Reason-Receipt is reserved in v2.4: the schema and the WithReceipt validator ship in this release, but production block paths leave the value unset until the receipt-pointer wiring lands. When populated, the value will be a 26-character Crockford-base32 ULID.

The reason vocabulary is closed. Examples by category include:

  • Egress. ssrf_private_ip, ssrf_metadata, ssrf_dns_rebind, domain_blocklist, scheme_blocked, subdomain_entropy, url_length, path_entropy, rate_limit, data_budget.
  • Content. dlp_match, prompt_injection, redaction_failure, media_policy.
  • MCP. tool_policy_deny, tool_poisoning, tool_chain_blocked, session_binding.
  • Posture. airlock_active, kill_switch_active, envelope_verify_failed, outbound_envelope_failed, redirect_scan_denied, authority_mismatch, session_anomaly, cross_request_deny, compressed_response, browser_shield_oversize.
  • Contract. contract_default_deny, contract_enforce_default, contract_non_default_port, contract_invalid_path, contract_observed_only.
  • Generic. parse_error, timeout, bad_request, pattern_unavailable, not_enabled, block_reason_overflow.

The full canonical list lives at docs/specs/block-reason-header.md in the Pipelock repo and in internal/blockreason/blockreason.go.

A block can have at most one reason code. The code is the primary signal. Severity and retry are advisory: severity tells the agent how loud to be when logging the block, retry tells the agent whether retrying with the same payload could ever succeed.

The same reason vocabulary is used for WebSocket close frames. MCP stdio does not have an HTTP header surface, so stdio blocks flow through the JSON-RPC error envelope instead.

Why the schema is small

Two design choices kept the vocabulary small:

  • No free-form reason strings. A free-form string would let the proxy tell the agent things like "request body contained AKIA...EXAMPLE at offset 1024." That is too useful for an attacker who controls part of the request. The agent learns exactly what the scanner saw and can adjust the next attempt to avoid the match. Closed vocabularies do not leak that detail.
  • No retry-after seconds. A retry hint with timing would let the agent build a retry policy that matches whatever the proxy is rate-limiting. The hint is categorical: transient says "retrying might work because the cause is not your request," none says "this exact request will never work," and policy says "this might work only after an operator changes policy."

Both choices trade specificity for safety. The agent gets enough signal to react sensibly without learning how to evade.

What changes for operators

The operator's experience changes from "decode the audit log against the agent's trace" to "read the block reason on the agent's HTTP response." Two examples:

A coding agent's CI pipeline started failing on a fetch tool. The pipeline log shows:

fetch tool: HTTP 403, body empty
agent: retrying (1/3)
fetch tool: HTTP 403, body empty
agent: retrying (2/3)
fetch tool: HTTP 403, body empty
agent: failed after 3 retries

Enter fullscreen mode Exit fullscreen mode

With the header on, the pipeline log includes:

fetch tool: HTTP 403, X-Pipelock-Block-Reason: ssrf_private_ip
fetch tool: X-Pipelock-Block-Reason-Severity: critical
fetch tool: X-Pipelock-Block-Reason-Retry: none
agent: not retrying (non-retryable block)
agent: surfacing block reason to user

Enter fullscreen mode Exit fullscreen mode

The agent stops retrying because retry is none. The user sees a meaningful error instead of an opaque pipeline failure. The operator does not have to decode anything.

Another example, MCP-shaped:

A new MCP server gets added to the agent's config. The agent calls a tool from it. Pipelock's tool-policy denies the call.

Without the header, the agent sees a JSON-RPC error with no actionable detail. With the header (or its JSON-RPC analog), the agent sees tool_policy_deny and can route around it: ask the user, fall back to a different tool, or surface the block reason directly. The behavior change is small in code but big in the agent's ability to recover.

Pairing with retry-budgeted agents

Modern coding agents have explicit retry budgets. A budget of three on a tool call burns fast when every block looks the same. With block reasons:

  • retry=none consumes zero budget on retry. The agent should not retry. Surface the block.
  • retry=transient consumes budget normally. The agent should retry with the same payload, possibly with backoff.
  • retry=policy is the operator case. The block may clear after a policy change, but the agent should not keep guessing.

For a coding agent with a budget of three retries, this means an agent that runs into a none block on attempt one fails in 100ms instead of three round trips. The retry budget is preserved for transient failures where retry actually helps.

The schema is open

The vocabulary, header format, severity values, retry semantics, and the WebSocket and MCP variants are all documented in the open-source Pipelock repo. Anyone who wants to implement this header in their own proxy can do so without a Pipelock dependency. Any HTTP client that wants to parse the header can do so without a Pipelock-specific library. The header values are short identifiers and fixed allowlist values.

If a competing agent firewall wants to adopt the same vocabulary, that is a feature, not a leak. A common reason vocabulary across vendors means agents can react sensibly regardless of which proxy is in front of them. The schema lives at docs/specs/block-reason-header.md. Take it, use it, propose changes via issue or PR.

Where this fits in the agent firewall stack

Block reasons are the debugging surface of an enforcement layer. The enforcement is the load-bearing piece: the agent firewall scans the request, the response, the headers, the tool calls, and decides allow or deny. The block reason is what makes the deny actionable.

A proxy without block reasons can still enforce, but every block is a black box. Operators get noisier audit logs and longer debugging cycles. Agents get worse retry behavior and more confused error messages. The header fixes both without weakening the enforcement.

If you are running Pipelock on main after 2026-05-02, the header is already on. If you are running an older release, v2.4 ships with the header on every block path, and the block reason headers guide walks the operator-facing changes. If you are running something else and your proxy gives you opaque 403s, this is the kind of feature worth asking for.