github luckyPipewrench/pipelock v3.6.0

latest release: v3
3 hours ago

[3.6.0] - 2026-10-01

Breaking Changes / Upgrade Notes

  • Building from source requires Go 1.26 or newer. Go 1.25 no longer receives upstream security fixes and current golang.org/x modules require 1.26. Release binaries are built with Go 1.26.8, the same toolchain the vulnerability scan checks, and CI tests Go 1.26 and 1.27. (#1670, #1673)
  • A config file that omits fetch_proxy.monitoring.blocklist now gets the shipped blocklist. Before this only the no-config path carried it, so a config file without the key (including the Helm chart's default values) ran with an empty blocklist. Destinations on the shipped list that were reachable under 3.5.0 will now block. Set blocklist: [] to keep it off. (#1655)
  • Request-body trust has its own list. On a destination in request_body_scanning.trusted_hosts, request-body injection and a fully redacted critical credential follow the configured action instead of the hard block. The request side no longer reads response_scanning.exempt_domains, so an operator who relied on a response exemption to relax outbound bodies must add the host to trusted_hosts. (#1454, #1637)
  • Host patterns are validated against how they're matched. Allow, deny, trust and bypass lists refuse a pattern that isn't an ASCII hostname before case folding, that carries a URL, host:port, fragment, interior wildcard or malformed label, or whose validated form differs from what the matcher compares. A destination with an empty DNS label (an extra trailing dot) is refused as invalid on every transport. (#1524, #1541, #1548, #1569)
  • tls_interception.passthrough_domains refuses a wildcard over a public suffix, including private-section suffixes shared by many tenants, because passthrough turns off body and response scanning for everything under it. List exact hosts or intercept with a trusted local CA. (#1595)
  • Temporary exceptions have a maximum expiry. Seven controls have horizons enforced at load and reload: request_body_scanning.sigv4_credential_routes[].expires and response_scanning.core_observe_exceptions[].expires (30 days), sandbox.best_effort_expiry (30 days), request_body_scanning.content_entropy_warn_routes[].expires and response_scanning.unscannable_passthrough[].expires (90 days), and fetch_proxy.monitoring.path_entropy_exclusions[].expires and fetch_proxy.monitoring.query_entropy_param_exclusions[].expires (180 days). The refusal names the field, date and maximum. A config-sourced sandbox expiry must be an absolute RFC3339 time; best-effort reason and expiry must come from one source, and per-agent sandbox overrides need their own reason and expiry within the same 30-day limit. (#1447, #1520, #1600)
  • session_profiling.volume_spike_ratio is removed. Nothing ever read it. A config that still sets it, top-level or per agent, is refused at load with a message naming the key. The canonical policy hash changes because the config shape changed; enforcement doesn't. (#1527)
  • A custom DLP pattern that reuses a core pattern name can't carry exempt_domains. Config validation refuses it at startup and reload instead of accepting an exemption the core floor never honored. (#1451)
  • pipelock run refuses file_sentry.action: block on the server listener. That listener has no child process to stop, so block could only log. A reload asking for it is rejected atomically. Subprocess MCP mode and action: warn are unchanged. (#1452)
  • Named agent policy follows bound identity only (Enterprise). A per-agent listener or source_cidrs match selects the named profile. A self-declared X-Pipelock-Agent name is still recorded for attribution but gets the fallback policy, and its session state folds to the client IP so rotating names can't reset adaptive scoring. (#1461, #1530, #1578)
  • contain install refuses to swap in a core build that can't enforce a named-agent config. Its pre-install check of the candidate binary requires the build to enforce every configured capability, so a working enterprise binary isn't replaced by one that can't enforce its listeners. A plain pipelock check on a core build still validates a named-agent config and exits 0. (#1629)
  • Each Pipelock process writes its own receipt chain. Processes sharing one flight-recorder directory no longer fork a chain; a restart is recorded as a signed link naming the tail it continues. verify-receipt --chain and evidence doctor follow the links. Tools that read a single session file must read the run chains instead. (#1654)
  • Activating the kill switch blocks held MCP approvals that haven't started sending. A held call that previously forwarded after activation now resolves as blocked. (#1699)
  • The default canonical policy hash changes because a signed GitHub release-download JWT may now reach release-assets.githubusercontent.com in an https URL query, and nowhere else. Receipt consumers that pin the default hash will see a new value. (#1730)
  • An approved MCP call that hasn't been sent yet is cancelled if the kill switch activates first. The client gets the same -32004 error as any other killed call, and the journal records a block with source kill_switch while the signed receipt records layer kill_switch. A config reload that turns the switch on applies it before the new proxy policy takes effect, including when the configured sentinel file already exists. Tools that group receipts by layer will see the new kill_switch value. (#1728)
  • A held HTTP call that the upstream gate refuses at release records resolution source upstream_contract. Readers of deferred-resolution records will see this new source. (#1728)
  • GitLab Private-Token and Job-Token headers are scanned for credentials by default. GitLab personal access tokens and CI job tokens sent to their issuer are still allowed; for a self-managed GitLab, list the host in dlp.gitlab_hosts so those tokens count as going to their issuer. Other GitLab token types stay blocked. (#1728)
  • Reading an SSH public key (id_*.pub under .ssh) no longer trips the built-in Credential File Access rule when the path is the call's only string value, on the MCP proxy, pipelock explain, the Scan API and hook events, including ~/.ssh spellings and keys that exist on the host. Shell commands, calls with another string value, any JSON key that names a protected path, and arguments nested too deep to inspect stay blocked. A custom rule that reuses the built-in name keeps the ordinary matcher. (#1726, #1734)
  • When MCP receipts are required, a request forwards only after its receipt is durably written. A failed or unsupported receipt write now blocks the request. (#1696)
  • audit-packet --offline no longer reports a packet as trusted. The Go, Rust and TypeScript verifiers return verdict: schema_checked_trust_unverified with trusted and valid false and exit non-zero. Separately, pipelock verify-receipt --clean-report output carries a required verification_mode (pinned_provenance or unpinned_structural). CI jobs that accepted the old offline success must require chain verification with an explicit trust anchor. (#1700)
  • The Scan API can return decision: "warn" for a tool_call that matches a warn-configured tool-policy rule, matching what the live MCP proxy does. Consumers that expected only allow or deny must handle warn. (#1572)
  • Shipped presets now set arg_source: patch_targets on tool-policy rules, and a binary older than 3.6.0 refuses to load them. The field is unknown to earlier releases, so copying a 3.6.0 preset onto an older binary, or sending a conductor bundle that uses it to an older follower, fails at config load instead of running without the rule. Upgrade the binary first, or keep older followers on bundles without the field. (#1557)
  • Contained hosts need pipelock contain install with the new binary to adopt the private network namespace, private temporary directories, the 0600 config mode and the refreshed CA export. (#1558, #1624, #1640, #1677)
  • The absolute-URI forward proxy hands an allowed redirect back to the client instead of following it. A client that follows redirects, such as a browser using HTTP_PROXY, now sends each hop as its own request through Pipelock, and each hop gets full admission, a new receipt and its own mediation envelope, with cookies staying at the issuing origin. A redirect that targets something Pipelock would refuse is still refused before the response is released, and the redirect check no longer spends rate-limit capacity for a request nobody sent. A 301, 302, 303, 307 or 308 whose Location is duplicated or carries a backslash, whitespace or a control character is blocked with a 403 and the reason ambiguous redirect location. /fetch still follows and rescans redirects itself. (#1749)
  • The redirect audit event now says redirect observed, not redirect followed. It fires when Pipelock sees a redirect, before it admits the target, and it isn't proof the target was contacted. Anything matching on the old message text needs the new one. (#1749)
  • With flight_recorder.require_receipts, a credential-audience, issuer-cookie or issuer-query allow forwards only after its receipt is durably written. A failed write blocks the request with reason receipt_emission_failed and layer credential_audience_receipt or issuer_allow_receipt. With require_receipts off these receipts stay best-effort. The receipt takes its policy hash from the config the request started under, not one a reload installed mid-request. (#1741, #1744, #1745)

Added

  • Contained agents run in their own network namespace. The agent reaches Pipelock and declared host services only through doorway sockets Pipelock forwards. The namespace is verified at launch, survives a binary swap and is restored on rollback. containment.published_services publishes agent services to the operator, and containment.display provisions a screen inside the namespace for a contained browser. (#1677)
  • pipelock contain view [--control] lets an operator watch or take over a contained agent's screen with any VNC client through a caller-only local socket. A view-only session can't type or click, and at most one controller holds the display. containment.display.geometry sets the screen size. (#1691)
  • Declared loopback services for contained agents. containment.loopback_services names each extra local destination with host, port, owner, reason and expiry. Replies are allowed only on the reply path, a privileged timer removes an entry when it expires, and contain verify fails when either half of a pair is missing or misordered. A contained agent can also reach its own subprocess on any loopback port while unrelated host listeners stay unreachable. (#1603, #1610, #1618, #1625)
  • pipelock contain run prints a session contract before launch listing the agent user, egress posture, capsule path, private /tmp state, tools and workspace grants with expiry. --dry-run runs preflight and prints it without launching, and expired grants refuse. (#1536)
  • A signed workspace change statement after each contained session, bound to the posture capsule, listing files the agent created, modified or deleted. pipelock posture verify --workspace-statement checks the pair and fails an incomplete statement with exit 2. (#1601)
  • Contained agents get private /tmp and /var/tmp through a transient systemd service, verified before launch. (#1558)
  • Browser CA trust for contained agents. contain install adds the interception CA to the agent's Chromium certificate database, verification reports it as probe 20, and rollback removes only what install added. (#1619)
  • systemctl reload pipelock waits for the verdict on systemd 253 and newer. The unit is Type=notify-reload and reports whether a reload was applied or rejected in systemctl status. (#1562)
  • pipelock contain doctor gains checks 7 and 8. Check 7 reads the managed chain structure on its own; check 8 checks that every managed doorway socket (the proxy doorway plus one per declared loopback service) is enabled and active. A definite agent bypass rule now reports FAIL instead of UNKNOWN. (#1560, #1659)
  • Credential audiences. Built-in provider keys, the chat-platform bot token, Slack tokens, Google OAuth bearer tokens and GitHub and GitLab tokens are allowed when sent to the API authority that issued them and blocked everywhere else, with an audited allow and receipt. The audience sets are compiled in and can't be set from YAML. dlp.github_enterprise_hosts and dlp.gitlab_hosts name exact self-hosted instances. (#1523, #1644, #1657, #1672)
  • AWS Signature Version 4 requests reach their own AWS endpoint. The Authorization header envelope and presigned-URL credential scopes are parsed to the shape AWS publishes, shared between the scanner and the redactor, and request_body_scanning.sigv4_credential_routes now applies on the reverse proxy too. (#1453, #1529, #1551, #1561)
  • Issuer-bound session cookies. With TLS interception on, a cookie is left out of header DLP only when the same session received that exact name and value from the same host over HTTPS and returns it within its domain, path and expiry. request_body_scanning.issuer_bound_session_cookies defaults to true. (#1676)
  • Issued paging tokens pass the query entropy gate. A query value that an intercepted JSON response from the same host handed out, such as a next-page link, isn't blocked by query entropy when sent back to that host in the same session. Search text is scored word by word. DLP still runs. (#1689)
  • response_scanning.core_observe_exceptions observes one core response pattern on one exact host with a reason, owner and expiry of at most 30 days. The pattern still runs and the finding is kept under its own core_observed reason; only the block is withheld. (#1620)
  • fetch_proxy.monitoring.path_entropy_exclusions exempts one host plus one literal path prefix from the path entropy gate only, and ships defaults for ordinary document-sharing links. Operator entries add to the shipped routes. (#1522, #1539, #1671)
  • Scoped block rules can exempt one exact JSON string value for a specified request. Missing, unreadable, ambiguous and other values stay blocked, including inside batch requests. (#1692)
  • mcp_tool_scanning.new_tool_admission (admit or withhold) governs a tool name that appears after the drift baseline is established. withhold keeps it out of the baseline until a signed listener drift reset. (#1584, #1588, #1598)
  • pipelock explain response reads a saved HTTP response body on stdin and reports which response pattern matched, in which scanner view, at which offset, with digests instead of the matched text. Response explanations also cover recorded findings and A2A response policy. (#1576, #1613)
  • pipelock verify-receipt --whole-recorder verifies every recorder entry, the receipt chain and the clean-shutdown transcript seal, and says which mode it ran. (#1573)
  • X-Pipelock-Receipt response header carries the action_id of the signed receipt that covers a response, set only after the receipt is recorded. Blocks set it on every transport; allows set it under flight_recorder.require_receipts. (#1486, #1604)
  • X-Pipelock-Shield-Rewrite response header lists the categories Browser Shield rewrote, and the fetch envelope carries the same value as shield_rewrite. (#1455)
  • Browser Shield can deliver SVG in sanitized form after the complete decoded body and its rewrite pass structural validation. Anything else refuses, and SVG stays refused while Shield is off. (#1661)
  • Scan API context.session_id accumulates across requests with the same bearer token, so a secret split across dlp, prompt_injection or tool_call requests is caught on the request that completes it. (#1582)
  • DNS-over-HTTPS is inspected as DNS. A strict RFC 8484 GET or POST message is checked by DLP and the entropy gate on every name, record payload and option. (#1683)
  • Partial disclosure of a known secret is detected. Eligible canary, environment and file values match contiguous disclosures of 16 bytes or more in plain or decoded text. Whole values spelled as decimal character codes also match; for environment and file secrets, decimal character-code matching requires the whole value. (#1494)
  • URL destinations inside query parameters are evaluated with the same allowlist, blocklist and SSRF checks as the outer host, through raw, percent, hex, base64 and base32 layers. (#1435)
  • Failed bearer guesses are rate limited per client address on the kill-switch API, Scan API, session admin API and MCP HTTP listener: ten wrong credentials in a minute get 429 with Retry-After. (#1555)
  • Integrations for Pi and Continue.dev. pipelock pi install configures a named proxy listener, and pipelock continue install wraps every MCP server Continue declares, both with remove and --dry-run. A Grok CLI guide covers forward-proxy and MCP wrapping. (#1476, #1487, #1589, #1602)
  • pipelock hermes install sets browser launch defaults that stop agent-browser advertising the automation marker, reported by hermes verify and reversed by hermes rollback. --no-browser-defaults skips it. (#1662)
  • pipelock doctor checks file-sentry coverage by walking the configured roots without installing watches. (#1498)
  • pipelock init --no-auditor skips the evidence auditor timer, and init now discloses the timer before installing it. (#1593)
  • Released Helm charts carry build provenance bound to the pushed digest, verifiable with gh attestation verify. (#1685)
  • License service: a 60-day self-serve Enterprise trial tier, operator commands to inspect, resend and revoke trial access, a provider-backed /ready endpoint, a customer self-serve resend endpoint that is off unless enabled, and license-service audit-summary. Provider API calls pin a dated version. (#1456, #1538, #1599, #1631, #1678, #1690)
  • pipelock contain run --lifecycle-output DIR writes a lifecycle.json report for the transient systemd service that runs the agent, bound to the unit's invocation ID, the launch arguments' digest, the config and policy hashes and the posture capsule digest. It records whether the service and its cgroup stopped, so automation doesn't have to treat a stopped systemd-run client as proof. It needs a new root-private directory, Linux with cgroup v2 and busctl, and an actual launch (--dry-run refuses it). Argument values, environment values and key material aren't written. (#1749)
  • Secret-egress evidence has a fixture-only contract. It defines typed decisions with a retained or redacted destination, an independent coverage model and a signed receipt payload kind, secret_egress_decision_v1, registered as fixture-only with a closed wire profile that the Go receipt parser and pipelock-verifier enforce. No Pipelock transport emits it yet. (#1737, #1740)
  • Containment conformance fixtures can supply nft chain text that runs through the same recognizer contain verify uses. (#1581)
  • whoami reports identity provenance (bound, self-declared or unknown) and resolves identity the way proxied traffic does. (#1591)

Changed

  • Release binaries are built on a Go toolchain with no reachable known advisories. (#1473)
  • Reverse-proxy traffic joins session enforcement. Session profiling, adaptive enforcement, cross-request entropy and taint apply to it, source CIDR bindings attribute it, and reverse DLP denials emit signed receipts. (#1477, #1532)
  • Listener and CIDR identities are graded bound, so audit, OCSF and CEF fields name them correctly. Bound identities no longer fill the shared per-IP burst bucket. (#1461, #1483)
  • Identical blocked retries add threat score once, and destination-scoped airlock transitions apply consistently and survive recorder replacement. session reset clears destination-scoped state. (#1531, #1534, #1607)
  • Response scanning skips patterns that can't match and runs the rest in parallel, without changing what it detects, and caches a clean verdict for an identical body per pattern set. Scans also share decoded and normalized views of the same input, and a response scan reuses matches it already found on that body, with findings and their order unchanged. There is no 3.5.0 baseline for a speed claim, and an identical large body is not served faster on the reverse proxy, so this entry makes none. (#1668, #1580, #1746, #1749)
  • Response injection patterns for system prompts, persistence and credential concealment need directive phrasing, so descriptive tool output passes while direct instructions still block. The core injection regex is one constant shared by the core floor, the defaults and every preset. (#1446, #1609)
  • Browser Shield reads HTML the way a browser does. It uses the HTML tokenizer, reads inline style with the CSS Syntax Level 3 algorithm, leaves JavaScript byte for byte under every JavaScript media type RFC 9239 lists, keeps interface markup in hidden application views, and stops breaking pages and bot checks. An intervention it can't record is refused. (#1570, #1616, #1641, #1666, #1682)
  • Response bodies are classified from their bytes, so image pixels and opaque binary data don't match prose-only detections while image metadata, embedded text and UTF-16 text keep being scanned. (#1634)
  • Compressed responses are decoded and scanned instead of refused for gzip and deflate, every Content-Encoding value is read, and an encoding with no decoder still fails closed. (#1642, #1650)
  • The fetch hidden-content surface is built from an HTML parse tree, and executable JavaScript bodies are no longer scanned as hidden page text. (#1623)
  • Block reasons name the finding that blocked. Query entropy blocks carry query_entropy, a body block names entropy only when entropy's own action blocked, rate-limit and data-budget blocks name the shared base domain, and scanner errors are reported as errors instead of injection detections. (#1475, #1638, #1663, #1667)
  • Audit mode observes request-body prompt injection. With enforce: false it follows request_body_scanning.action, and each finding is reported once. (#1681)
  • The core injection scanner runs when response_scanning.enabled is false on reverse-proxy response bodies and pre-action hook payloads. Pre-action hooks scan submitted commands, tool arguments and supplied file content; they don't intercept the later tool response. (#1628)
  • A2A agent-card drift judges what a change introduced. Structural changes still block; a descriptive change blocks only when it introduces a new cue class. (#1537, #1633)
  • Tool policy recognizes namespaced tool names and terminal execution in chain detection. (#1571)
  • Hot-reload refusals name the field that would have weakened a required mode and say a restart applies it. Rejected trust expansion is explicit in diagnostics. (#1478, #1652)
  • Host sets are canonicalized (case, duplicates, trailing dots, IDNA) before policy hashing and matching. (#1460, #1569)
  • Conductor followers require a verifiable version before applying a bundle with a minimum version, restore and re-verify cached policy before serving, keep live and durable policy consistent, and bind rollback authorizations to their target stream without resetting the replay counter. (#1471, #1627, #1630, #1693)
  • Receipt resume across a key change trusts only a key this process loaded earlier in the same run. Final checkpoints survive shard rotation, and non-Rekor anchor proofs are verified before they're persisted. (#1535, #1577, #1687)
  • The external action grant verifier's contract now requires not_before and caps a grant's lifetime at five minutes, a Pipelock limit informed by the short token lifetimes the OAuth Transaction Tokens BCP recommends, and refuses with not_yet_valid or lifetime_exceeded. The signed references it checks may declare jcs-rfc8785-nfc canonicalization. No shipped configuration enables the verifier yet. (#1575, #1617)
  • MCP startup is recorded. Initialization and tool-list requests and the initialized notification record correlated outcomes, and a required recording failure stops forwarding. (#1467)
  • Emitter and SIEM health snapshots publish counters, degraded state and error details together. (#1481, #1484)
  • Every dropped DLP finding is recorded in pipelock_dlp_dropped_matches_total and a dlp_warn audit line. (#1458)
  • Containment probes that can't establish a cause report UNKNOWN with a non-zero exit, Podman is recognized, and the managed nftables ruleset is reconciled instead of appended. (#1466, #1507, #1621)
  • The dashboard requires exactly one Authorization header, and an embedder that disables its own auth must name the outer boundary. (#1474, #1553, #1590)
  • Setup integrations parse Codex warnings separately, find packaged Claude Desktop on Windows, preserve runtime-resolved VS Code configuration, replace recoverable older wrappers, and report bundle-load problems from every hook. (#1465, #1546, #1554, #1568)
  • pipelock hermes install and rollback take an exclusive lock so two runs can't interleave. (#1665)
  • File-sentry startup failures name every unreachable subtree and summarize the remedies first. (#1498, #1545)
  • pipelock init waits briefly for user systemd before skipping the evidence auditor. (#1626)
  • A rules bundle fetch blocked by Pipelock says so and names the reason. A bundle whose min_pipelock can't be checked on a development build warns and loads. (#1447, #1540)
  • The sandbox bridge closes relays idle in both directions after the largest configured proxy idle bound. (#1701)
  • The sandbox launches on busy desktops: the shared-UID task ceiling rose from 4,096 to 8,192, a launch adds at most 1,024 tasks above the current count, a stricter inherited limit still wins, and a launch at the ceiling is refused. (#1707)
  • contain install checks its prerequisites before it changes the host. Preflight now looks for certutil and for an agent tool to allow-list, and judges whether pipelock-agent can run that tool by walking its path the way the kernel does, including symlinks, . and ... Before, the install failed at a late step and rolled back everything applied up to it. A reinstall keeps an existing add-tool allow-list when no default tool resolves, and an allow-list file that pipelock-agent can't read is repaired. (#1723, #1725)

Fixed

  • The contained systemd service starts again. contain service-posture runs its host-side checks from the host network namespace, where the managed nftables table is visible, so the documented ExecStartPre recipe works. contain run no longer requires a published listener before the agent that creates it has started. (#1759, #1770)

  • An expired loopback or published service entry no longer takes the proxy down. An expired containment.loopback_services or containment.published_services entry is dropped and reported at load, startup, reload, install and nft reconciliation instead of refusing the whole configuration. Other entries keep working, a malformed entry still rejects, and contain verify still fails and names the expired entry. Retiring a forwarder also stops the relay its socket started, and cleanup only touches units whose definitions match what Pipelock installed. (#1759)

  • contain doctor checks the in-namespace forwarder services, and doctor and verify give remedies that work when a unit is masked or a relay still holds the port. (#1759)

  • A promoted Learn and Lock manifest applies live in pipelock mcp proxy and pipelock run without a restart. A rejected promote keeps the last accepted contract and is logged. (#1759)

  • pipelock init checks the saved or retained configuration, and prints follow-up commands with that exact config path. Malformed retained files no longer pass checks against an unused preset; dry runs continue to check the proposed config. When the canary is blocked before DLP, init names the scanner that blocked it. (#1758)

  • pipelock canary prints a JSON snippet that loads as config, and returns an error when either output format can't be written. pipelock posture verify shows elapsed age in whole days and unknown when the generation time is missing; expiry checks are unchanged. (#1761)

  • The reverse proxy now hard-blocks a critical credential in the URL path or query in enforce mode, even when request_body_scanning.action is warn. Before this fix, the shipped presets forwarded such a request upstream while /fetch and the forward proxy refuse the same key. A body block on the reverse proxy now also counts toward the session's adaptive score, as URL and header blocks already did. (#1748)

  • Reverse-proxy response blocks now carry the X-Pipelock-Block-Reason header set, as the block-reason documentation says. Injection, compressed-response, oversize, scan-failure and media-policy blocks used to return the reason only in the JSON body. (#1748)

  • The adaptive escalation table is corrected: the levels are reached at 1, 2 and 4 times escalation_threshold, not 1, 2 and 3. The code always doubled the threshold after each escalation; the documentation was wrong. (#1748)

  • A kill switch activation now cancels held MCP calls within about a second, for every source including sentinel_file. It used to wait for the next request or for the resolver to finish. (#1748)

  • A tool call or A2A request the kill switch refuses on MCP stdio, the stdio-to-HTTP bridge, the WebSocket proxy and the HTTP listener now leaves a signed kill_switch block receipt. A refused JSON-RPC batch is receipted for each tool call and A2A member, up to 64 per batch (a warning says how many were not), and is answered with one -32004 error per member that has an id; the HTTP listener used to answer a refused batch with 202 and no receipt, and the other transports dropped it without a response. (#1748)

  • The deferred-action API reports the decision actually applied. An approve that arrives after the kill switch activated, or that a release check refuses, now returns final_decision: "block" instead of allow. (#1748)

  • The deferred-action API also reports final_decision: "block", and the journal records a block, when a required receipt for the release cannot be written. The call is not sent, but the approve response and the journal said allow. The allow receipt is now written after the journal accepts the allow, so a journal that cannot be written leaves a block receipt alone. With one receipt format the chain never holds an allow for a call that was not sent; with both formats configured, a failure in one leaves the other format's allow, followed by the block. (#1748)

  • The reverse proxy's policy-replay record now matches the verdict it enforced. A critical credential in the URL or body that was hard-blocked under a warn action was recorded as a warning. (#1748)

  • The reverse proxy, the forward proxy and TLS interception no longer relay an upstream X-Pipelock-* header to the client. A 103 Early Hints response, a final response header, or a declared or undeclared trailer could carry a forged X-Pipelock-Block-Reason or X-Pipelock-Hint. Reverse-proxy block responses also no longer announce upstream trailers, and an upstream 103 no longer drops the X-Pipelock-Receipt handle from the reverse proxy's final response. (#1748)

  • pipelock-verifier independent --require-full-coverage fails when the anchor covers fewer receipts than the chain holds. The default is unchanged: receipts after the anchor are chain-verified and reported through covered_receipts, chain_length and tail_chain_verified, and a broken tail exits non-zero. (#1748)

  • The upstream_contract recipe now says how a contract rule names the bridge's upstream and what changes the release check's outcome. The Learn and Lock guide says a promoted manifest applies live, without a restart. The kill switch API and pipelock mcp proxy documentation says which routes exist there. (#1748, #1759)

  • A site whose first address is unreachable now connects through the next validated address instead of failing with 502. The proxy's and the SIEM forwarder's SSRF-safe dialers validated every resolved address but connected only to the first; they now try the validated addresses in resolver order and still refuse the host before any connect if any address is blocked. (#1748)

  • A clean reverse-proxy response that takes longer than 30 seconds to fetch and scan is delivered instead of ending in an empty reply. The listener's fixed 30 second write timeout cut off clean 48 MiB and 64 MiB responses from a size_exempt_domains host, which scan at roughly 0.7 to 1.3 seconds per MiB. The listener now bounds a client that stops reading, and caps how long one response may spend being written (10 minutes from its first write, with the 30 second stall window never extending past it) and how many connections it holds, instead of bounding scan time. A silent upstream is bounded by a 120 second response-header timeout. The response size ceiling and what is scanned are unchanged, and a response over size_exempt_scan_max_bytes is still refused with a 403. (#1748)

  • pipelock-verifier independent accepts an anchor bundle after the receipt chain has grown. It compared the bundle's checkpoint with the whole chain, so every bundle stopped verifying as soon as one more receipt was written. It now recomputes the checkpoint over the receipts the bundle covers, fails a chain shorter than the bundle claims, and verifies the receipts after the covered prefix as a chain: a broken tail exits non-zero and is not reported as valid. The verdict adds covered_receipts, chain_length, and tail_chain_verified, and the OK line names the coverage. (#1748)

  • Claude Code's Read and Grep tools now go through the same credential-path policy as other file reads. They used to fall through to the generic content-only scanner, so reading an SSH private key, ~/.aws/credentials or /etc/shadow with Read was allowed even though the same path was already blocked for Bash and MCP tools; Grep gets the same check when its path names a credential file, and a Grep over ~/.ssh, ~/.aws or a directory containing them, such as your home directory, is refused, as is one over a directory that contains /etc/shadow (/etc or /). A Grep that names no path is judged from its working directory. Both tools keep the content scan they had before, and NotebookEdit with a notebook path goes through the same write checks as Write and Edit. Reading an SSH public key (id_*.pub) stays allowed, matching the existing Credential File Access exception. (#1739, #1743)

  • A hard link to a file inside a protected directory is now treated as that file. Hard links to individual protected files such as ~/.aws/credentials were already matched; a link to a file under ~/.ssh, ~/.config/systemd/user, ~/Library/LaunchAgents, the cron, init and systemd directories, /var/log or Pipelock's state directory was not. Claude Code's Read and single-file Grep, the Cursor read hook and local MCP file tools now match it, including a link to a file in a subdirectory, such as a unit drop-in, and under both $HOME and the home the account database records. The check runs only for a regular file that has another link. It walks each protected directory without following symlinks, skips anything on a different filesystem (a hard link cannot cross one), and treats a directory it cannot read in full on the same filesystem, or a tree of more than 8192 entries, as holding the file, whoever owns it. A multi-link file on the same filesystem as an unreadable protected directory is therefore refused even when it is unrelated. Each protected directory is walked once per tool call, however many path values the call carries. (#1748)

  • Relative paths in the Claude Code and Cursor hooks are resolved against the session's working directory, the cwd in the hook payload, not the directory the hook process was started in. A relative path through a symlink was checked against the wrong directory when the two differed. (#1748)

  • The Claude Code Grep credential-directory check no longer depends on $HOME alone. It also resolves the home directory recorded for the user the process runs as, checks both when they differ, ignores a $HOME that is not an absolute path, and still refuses when neither can be resolved. (#1748)

  • The SSRF-safe dial fallback is bounded. The proxy and the SIEM forwarder try at most three validated addresses, in resolver order, and stop when the request's deadline passes, so a name with many unreachable records no longer holds a connection for one dial timeout per record. (#1748)

  • pipelock pi remove reports what it actually did to Pi's settings file: restored a proxy setting Pi had before install, removed the proxy setting pipelock added where none existed, or deleted a settings file pipelock created. It used to always claim a restore, which was wrong in the other two cases. (#1739)

  • pipelock hermes rollback and containment rollback remove Pipelock's browser launch flag after you edit the agent-browser args, keeping your edits, including arguments you removed. Hermes rollback names the flag it removed and the backup it saved. If you already removed the flag by hand, rollback clears its ownership record, so a later install no longer refuses with a stale-record error. (#1736)

  • Release downloads through the proxy work again, and a block for an oversized response names only settings that can lift it. Outcome receipts record exempt_over_cap_unscanned or incomplete instead of complete when an exempt stream passes the scan ceiling or breaks mid-transfer. (#1730)

  • A real GitHub release download also works on the fetch, forward-proxy, and TLS-intercepted paths that scan its redirect, not only the shape without an Azure SAS signature. GitHub's redirect carries a signed Azure SAS beside its download grant; the SAS is now admitted only next to that same verified grant for the exact storage host, with GitHub's full signed SAS parameter set present, each field in the format Azure documents and exactly one sig parameter in the query, so a SAS without a valid co-located grant, on another host, or over plain HTTP still blocks. (#1739, #1743)

  • Text DLP no longer reads joined English words as an AWS access key ID. Real AKIA and ASIA keys split by spaces or invisible characters still block. (#1730)

  • Each distinct dropped request-body DLP value is recorded once per request. A disabled or suppressed pattern used to add two or more to pipelock_dlp_dropped_matches_total and write repeated dlp_warn lines when redaction or the subdomain view re-reported it. Two different values, including two different seed phrases, are still counted separately; the same value repeated in one request is one record. (#1736)

  • The 30-day limit on response_scanning.core_observe_exceptions expiry is enforced at startup and by pipelock check, not only on hot reload. A fresh start used to accept an exception months ahead that a reload of the same file refused. (#1732)

  • pipelock guard and pipelock sandbox work on hosts whose CA bundle is a symlink outside /etc/ssl/ and /etc/pki/, such as Arch Linux. Guard refused every launch as a narrowed policy, and a sandboxed process couldn't read its CA bundle. Both now grant the CA files as exact files; Guard still checks the resolved target against its compiled floor, and neither command grants the target's directory. (#1733)

  • Request-body DLP inspects every Content-Encoding a request declares. A credential header approved for one host is checked again when a redirect crosses to another host. A query with a malformed percent escape is entropy-checked as sent instead of skipping inspection, and query-entropy exclusions still apply. (#1728)

  • Every receipt verifier checks the file the operating system actually opens. A path such as link/../receipt.jsonl was cleaned as text first and could verify different bytes from the resolved file; explicit files now follow the symlink before .., a symlinked evidence directory is refused, and the reference CLI, pipelock-verifier, and the TypeScript and Rust verifiers agree. Recorder timestamps are parsed from their original token, so Go 1.27 no longer accepts a timestamp Go 1.26 and the TypeScript verifier reject. (#1717)

  • Default-on settings stay on when a config file omits them. Enabling Browser Shield from a config file now strips hidden prompt traps, extension probes and tracking pixels as documented, and request_body_scanning.issuer_bound_session_cookies is on for config files as well as the no-config path. An explicit false is still honored. (#1703)

  • Tool policy protected-path rules cover equivalent operations. Move, rename, copy, delete, permission-change and link-creation tools now match rules that protect a destination path, patch targets come from patch headers, and backslash separators are recognized. (#1557)

  • Tool policy matches the file a local path argument resolves to. For a subprocess MCP server on the same host, or a Claude Code or Cursor hook, a symlink, hard link or relative name that reaches a protected file is matched as that file, and the shell startup-file and audit-log rules catch link-creating ln, link and cp commands in every preset. Remote upstreams and shell command text are still matched as written. (#1718)

  • A release stays a draft until its Helm chart attestation succeeds. The GitHub Release, the Homebrew formula and the floating major tag publish only after it, so a failed attestation stops the release before it's public. (#1716)

  • A containment install that fails partway restores what it changed. A step that edits the nft rules and their units, the integrity pin, the login profile script, the credential guard's service state or the evidence directory's access list and then fails reports the change so rollback restores it, and a rollback that can't finish is reported with the install error instead of only printed. (#1716)

  • The core DLP floor covers MCP input and the A2A-only forward branch, so a core credential in a tool call blocks even where the preset warns. (#1528)

  • MCP scanning covers content it used to skip: numeric leaves and tool definitions in responses, structured values under media-typed fields, the whole forwarded JSON-RPC envelope and session header, structuredContent keys, and SSE events with no data line. The listener enforces its state-token requirement on its own. (#1493, #1521, #1694)

  • A2A header scanning checks every A2A-Extensions line. (#1686)

  • Cross-request detection uses a keyed per-session bucket digest, requires classified identities, inspects a completing fragment before eviction, keeps exact contributors, clears the keys production writes on reset, and keys MCP state on the session key alone. Cross-request blocks emit receipts with a neutral client message. (#1489, #1517, #1526, #1547, #1563, #1579)

  • Partial (206) responses fail closed when Shield, media stripping or redaction would change their bytes, and decoding or truncating a partial body is refused. (#1653)

  • Empty and mistyped media responses pass or block with a named reason instead of a parse error. (#1643)

  • False positives in ordinary work: spaced assignments as URL credentials, prose read as AWS resource IDs, environment lookups in tool commands, base64 identifiers, bot checks, the jailbreak pattern inside encoded data, percent-encoded query exclusion keys, OAuth redirect_uri, PKCE challenges and static asset hashes, including short hyphenated build hashes. (#1482, #1664, #1669, #1671, #1680, #1683, #1757)

  • WebSocket relays end as soon as either side leaves instead of waiting out the idle timeout, and scoped DLP controls apply to frame scans. (#1604, #1674)

  • Go 1.27 compatibility fixes preserve signed evidence when it records invalid UTF-8. (#1673)

  • TLS interception finds its default CA in the Pipelock home (--home, PIPELOCK_HOME, then ~/.pipelock) and refuses to guess when two exist. (#1651)

  • Containment hardening: the per-agent listener accepts only the relay account and root, the managed display requires X authorization, the exported CA stays current, NODE_EXTRA_CA_CERTS uses the combined bundle, the config installs at 0600 so admin commands work, and YAML null or aliased containment settings count as absent. (#1624, #1635, #1636, #1640, #1679)

  • A contained agent's listener refused for a missing license now says so. Without a license, named agent profiles are disabled at load, and a containment.agent_listener pointing at one was refused as undeclared. The config is still refused, but the error names the profile and says named profiles need a Pro license with the agents feature. (#1720)

  • A provider key sent to its own issuer passes query entropy when the entire value is one built-in credential that DLP already allows for that destination. Extra bytes, other parameters and every other scanner are unaffected, and the semicolon-separated query path stays strict. (#1708)

  • An allowed image served under the wrong raster type is classified by its complete image header, handled like the same bytes correctly labeled, and forwarded with the proven type in Content-Type, the fetch JSON response and MCP mediaType/contentType. (#1708)

  • pipelock contain view reaches the display viewer again for the configured operator, and a viewer socket permission error names both possible causes. (#1708)

  • A long high-entropy environment value no longer stops Pipelock from starting. A known value over 4,096 bytes, such as an inline certificate or JSON key under scan_env, is indexed at up to 4,081 evenly spaced positions instead of being refused, so a copied fragment is still caught once it covers one of them. A long URL-shaped value is sampled as one value in source order. (#1712)

  • Mislabeled JPEG and PNG bodies are refused when their actual format is disallowed, including when metadata stripping is off on the proxy or MCP path. (#1711)

  • Issuer-returned query values use the URL entropy gate's exact-session allowance for same-origin JSON links and redirect locations. The two OAuth authorization-code redirects can carry their issued state and nonce, or code, state and iss, between the client and the authorization server when the session declared that callback; any other parameter keeps the ordinary gate, the relief is off when request_body_scanning.content_entropy_action is block, and DLP and the other URL checks still run. (#1711)

  • MCP subprocess handling: descendant cleanup is reported before startup, active approval resolvers survive child cleanup, binary locations report unknown instead of not-suspicious, and tool rules reload from one snapshot. (#1516, #1550, #1586, #1596, #1639)

  • Reverse-proxy request blocks score each distinct finding for adaptive enforcement. Every URL, header and body block was recorded under one shared scanner name with an empty reason, so a different finding against the same upstream looked like a retry of the first and the session score never climbed from reverse-proxy blocks alone. Identical retries still score once. The receipt and metric layer stays dlp. (#1755)

  • A crash that leaves a partly written last line in an evidence shard no longer wedges the next reload. Pipelock never appends to or rewrites the damaged shard. A reload that meets one starts a fresh run session and publishes the config only after the new receipt emitter opens, and a local anchor log with a torn final write continues in a numbered .segment- file beside it. Malformed complete records, bad signatures and broken hash links still reject the reload. pipelock evidence doctor reports the shard as damaged, evidence_health.torn_tails and torn_tail_present in /stats record it, and pipelock_evidence_torn_tails_total and pipelock_evidence_torn_tail_present expose it for alerting until the process restarts. (#1750)

  • Live receipt auto-anchoring no longer loads the whole chain into memory. Checkpoints are built by streaming the chain, with bounded temporary spill files that are cleaned up and swept when stale, and the full chain on disk is still verified every time. (#1754)

  • The reverse proxy gives each upstream response its own copy. Closing a blocked response's body early let net/http's background drain write upstream trailers into the same struct the proxy was rewriting, a data race. Trailers on clean responses are still relayed. (#1751)

  • Browser Shield strips a comment trap without eating the markup between separate comments. Matching now happens inside each parsed comment, so stylesheets and scripts between two comments stay intact, and XML processing instructions, SVG and MathML content and an unterminated instruction are handled. (#1753)

  • pipelock sandbox --strict lets native runtimes create threads. Strict seccomp now answers clone3 with ENOSYS instead of EPERM on linux/amd64, so glibc retries with the argument-filtered clone call. clone3 still never runs and namespace-creating clone stays denied. (#1749)

  • Evidence auditor targets survive init reruns. (#1490)

  • The runtime fails closed on a reload that fails partway, leaving the behavioral baseline action unchanged, and the support bundle refuses an output path that already exists. (#1696)

  • Go, TypeScript and Rust verifiers check every present receipt chain and recorder boundary in directory, named-run and file modes. They agree on key rotation and unpinned signature checks, and a failed audit packet can't report trusted evidence. The anchor-bundle schema loads under strict validators. (#1611, #1656, #1658, #1697, #1713)

  • Rekor anchoring accepts sharded logs whose entry index differs from the tree index. (#1470, #1622)

  • License service: one active trial per email across writers, the trial slot table as the only eligibility authority, atomic webhook revocation, and removal of a stale founding deadline. (#1556, #1597, #1606, #1698)

  • The GitHub Action keeps repository-derived text out of workflow commands. Annotations and the job summary escape it, the audit report no longer prints file names into the job log, and config validation output runs with workflow commands stopped. (#1698, #1704)

  • Playground: the durable signing root stays off visitor VMs, delegated runs seal against their root, published replays stay verifiable, a broker without a usable root refuses to start, visitors get their own evidence unedited, and kits stay downloadable. Runs record the requested and provider-reported model, and the broker's admin listener, key cache, artifact reads and VM slots are hardened. (#1442, #1444, #1449, #1450, #1459, #1583, #1646, #1647, #1649, #1695)

  • The receipt example's tamper step edits a signed field, so it now proves tamper detection. (#1445)

  • The logo PNG renders transparent and scaled, with a generated raster ladder and .ico. (#1519)

Removed

  • The never-imported internal/abom and internal/manifest packages. (#1518)

Documentation

  • Onboarding checks state their scope. Init's local canary and verify-install's temporary-proxy fixtures distinguish synthetic verification from real-client routing, with configuration provenance in human and JSON output. False-positive recovery starts with an explanation, the smallest applicable correction and a recheck. (#1758)
  • Install and release examples, receipt anchoring and SCITT scope, and the MCP upstream SSRF exception are corrected. (#1431, #1432, #1468, #1594)
  • The internal release checklist moved out of the public repository. (#1608)
  • Configuration, tool policy, receipt verification, flight recorder, integration and containment guides are corrected to match current behavior, including that a promoted Learn and Lock manifest applies live, and that the sandbox guide covers Ubuntu's AppArmor user-namespace restriction. audit-packet --help says trust needs --key or --expect-sha256. The kill switch docs count seven activation sources, including uncertain Conductor apply, which also ignores IP allowlist exemptions. Mediation envelope examples are checked against the production serializers, and an examples index lists each example's runtime requirements and verification command. (#1736, #1748, #1755, #1759, #1764, #1766)
  • The contain guide lists what the signed workspace change statement records, which is paths, counts and completeness and no per-file sizes, timestamps or digests. (#1729)
  • A browser reproduction guide describes a synthetic diagnostic runner and says standalone sandbox --strict doesn't support Chromium's own sandbox. The forward-proxy redirect, mediation envelope and transport guides describe the client-followed redirect behavior. (#1749)
  • The WebSocket proxy example's verify script stops its echo helper when it exits. (#1735)

Dependencies

  • The standalone receipt verifier packages move to 0.4.1 (@pipelock/verifier-ts on npm, pipelock-verifier-rs on crates.io) so they are built from the same verifier source as this release. (#1771)
  • Weekly dependency updates, fast-uri 3.1.8 in the TypeScript verifier, and @unicode/unicode-15.0.0 2.x. (#1416, #1441, #1469, #1492, #1742)
  • The init and license-service images use Alpine 3.24.2 (OpenSSL 3.5.8), and the source Dockerfile builds with Go 1.27.1. (#1716)

Testing and CI

  • Fail-closed and error-path coverage across signed evidence, receipts, anchoring, containment, deferred resolution, sessions, guard setup, sidecar topology, config parsing, TLS files and rule updates. (#1497, #1499, #1500, #1501, #1502, #1503, #1504, #1505, #1506, #1508, #1509, #1510, #1511, #1512, #1513, #1514, #1515, #1549, #1552, #1574, #1585, #1684)
  • Flaky and timing-dependent tests fixed, including scanner drain, debounce, deadlines, subprocess timeouts, calendar-dated fixtures, sandbox bridge keep-open and contained display tests. (#1438, #1439, #1457, #1462, #1463, #1464, #1485, #1488, #1491, #1544, #1587, #1605, #1645, #1705, #1715)
  • New benchmarks and transport proofs: response scanning at real page sizes, and identity-encoded scanned responses for browsers. (#1612, #1660)
  • The Gauntlet benchmark gate owns Pipelock's acceptance policy and tracks the current bench revision and record schema, and now pins Agent Egress Bench v1.1.0 (corpus v2.14.0) with a matching baseline and acceptance record. (#1433, #1434, #1436, #1440, #1443)
  • CI runs the Go floor on pull requests, proves both supported Go versions on main, proves compatibility gates fail closed, keeps full timeout diagnostics, and warms the MCP end-to-end package before timing. (#1448, #1480, #1592, #1614)
  • The proxy and scanner race-test shards are each split in two by test name, bringing both back well under the 20-minute shard timeout. The last half of each skips exactly what the first runs, so a new test still runs once, and CI fails if a shard command stops passing its selector. (#1721)
  • The Python, Rust and TypeScript verifier corpus tests run every AARP corpus category and fail when one isn't wired in, and the benchmark gate fails a full run when a baseline benchmark disappears or rises from a zero baseline. (#1722)
  • The pull-request AI review shows unfinished reviews on the current head, stays informational, retries rate-limited chunks, and uses current model tiers and budgets. (#1472, #1525, #1533, #1542, #1543, #1559, #1648)
  • An internal review record was removed from the source tree. (#1719)

📚 Docs: https://pipelab.org • 💬 Community: https://discord.gg/badNfhGKTc

Pipelock is an open-source agent firewall. Come poke holes in it.

Don't miss a new pipelock release

NewReleases is sending notifications on new releases.