Agent Skills in the Cloudflare Agents SDK are now files. Write a folder with a SKILL.md into your agent's Computer ↗︎ Workspace, under /workspace/.agents/skills/, and the agent can use it on its next turn. You do not need a build step or a deploy, and the agent can write its own skills.
Three sources. Load skills from a Computer Workspace, an R2 bucket, or code. Think lists every source again before each turn.
The open format. Every SKILL.md is checked against the Agent Skills specification ↗︎. As the specification's guide for agents recommends, a skill that breaks a rule still loads, with a logged warning. Use parseSkill() to check a skill before you write it.
Scripts in the Workspace. Skill scripts are Workspace files, and the model runs them with the Workspace exec tool. Refer to Execution.
Skill tools for any harness. Use the same activate_skill and read_skill_resource tools in Think, Pi Durable, or the AI SDK. Refer to Skills tools.
Warm prompt caches. When a skill changes mid-conversation, Think sends the new catalog as a reminder instead of rebuilding the cached system prompt. An overridden getSystemPrompt() now stays at the top of the system prompt, above context blocks.
This is a breaking change for code that uses agents/skills directly. The agents:skills Vite import, agents/skills/compile, SkillRegistry, and Think's skillWorkspace are removed, and runner() moved to agents/skills/runner. Install Agents SDK v0.32.0 ↗︎ and Think v0.23.0 ↗︎ or later. Refer to the computer-skills ↗︎ and r2-skills ↗︎ examples.
MCP tool-call analytics support a response_status query parameter on the account, portal, and server timeseries endpoints. Use success, error, or cached to count one status, or all to include every status.
The default is success. Previously, counts included failed and cached calls. If you need the previous all-status counts, set response_status=all explicitly:
GET /accounts/{account_id}/access/ai-controls/mcp/analytics/tool-calls/timeseries?response_status=all
The response includes the selected response_status. Invalid values, including an empty value, return 400.
Agents that build websites need to check their work in a real browser. Internal hostnames let your agent's browser preview a dev server running in a container without exposing that server to the public Internet.
With internal hostnames, the persistent browser in the Cloudflare Agents SDK can open sites that only your agent serves. Give the browser hostnames such as preview.internal, and its requests to them go to your agent instead of the Internet. The model can check a dev server in its sandbox, a folder in its workspace, or another Worker in the same browser it uses for the web, and nothing is exposed publicly.
Each internal host has a fetch function that runs in your Durable Object, so it can use the object's bindings and state. A host can also send its requests straight to another Worker through a service binding. Add a target schema to a host, and the model chooses what it serves, such as which port or which folder.
Internal hostnames are a capability of the browser, independent of your agent harness. Use the built-in adapters for Pi Durable, AI SDK, and TanStack AI, or connect another harness by implementing a tool with browser.session(). No prebuilt adapter is required.
This example adds a private dev-server preview to a Pi Durable agent. Start with that guide's packages, bindings, and request handler, and install Agents SDK v0.31.0 ↗︎ or later. The example has three parts:
Browser configuration:internalHosts defines how the browser reaches the dev server.
Durable Object wiring: Lifecycle and fetchInternalHost() connect the browser to your application.
Harness integration:browserTool from agents/browser/pi registers the tool with Pi.
import { DurableObject } from "cloudflare:workers";import { createModels } from "@earendil-works/pi-ai/models";import { createRegistry, Harness } from "@earendil-works/pi-durable";import { Browser, browserRun } from "agents/browser";import { browserTool } from "agents/browser/pi";import { PiHarness } from "agents/harness/pi";import { Lifecycle } from "agents/lifecycle";import { createAI } from "agents/models/pi-ai";import { z } from "zod";// Your own code: reach a port on your sandbox and return its Response.import { fetchSandboxPort } from "./hosts";// Browser Run calls back into your Worker through this entrypoint.export { InternalHostRouter } from "agents/browser";export class BrowserAssistant extends DurableObject { ai = createAI({ binding: this.env.AI }); registry = createRegistry(); browser = new Browser({ provider: browserRun(this.env.BROWSER, { internalHosts: { router: this.ctx.exports.InternalHostRouter, hosts: { "preview.internal": { description: "Your dev servers", // What the model passes to browser.assignHost() to choose // what this host serves. fetch receives it as its second argument. target: z.object({ port: z.number().int().min(1024) }), fetch: (request, { port }) => fetchSandboxPort(port, request), }, }, }, }), }); harness = new PiHarness({ harness: ({ storage, context, onWake }) => { const models = createModels(); models.setProvider(this.ai.provider); this.registry.install({ name: "browser", tools: [ browserTool({ browser: this.browser, loader: this.env.LOADER }), ], }); return Harness.open( storage, { models, registry: this.registry, onWake }, context, ); }, defaults: { model: this.ai("@cf/zai-org/glm-5.3-flash") }, }); lifecycle = Lifecycle.install(this).use(this.browser).use(this.harness); // InternalHostRouter calls this method to reach your Browser. fetchInternalHost(message) { return this.browser.fetchInternalHost(message); } async ask(prompt) { const { text } = await this.harness.prompt(prompt); return text; }}
import { DurableObject } from "cloudflare:workers";import { createModels } from "@earendil-works/pi-ai/models";import { createRegistry, Harness } from "@earendil-works/pi-durable";import { Browser, browserRun, type InternalHostMessage } from "agents/browser";import { browserTool } from "agents/browser/pi";import { PiHarness } from "agents/harness/pi";import { Lifecycle } from "agents/lifecycle";import { createAI } from "agents/models/pi-ai";import { z } from "zod";// Your own code: reach a port on your sandbox and return its Response.import { fetchSandboxPort } from "./hosts";// Browser Run calls back into your Worker through this entrypoint.export { InternalHostRouter } from "agents/browser";export class BrowserAssistant extends DurableObject<Env> { ai = createAI({ binding: this.env.AI }); registry = createRegistry(); browser = new Browser({ provider: browserRun(this.env.BROWSER, { internalHosts: { router: this.ctx.exports.InternalHostRouter, hosts: { "preview.internal": { description: "Your dev servers", // What the model passes to browser.assignHost() to choose // what this host serves. fetch receives it as its second argument. target: z.object({ port: z.number().int().min(1024) }), fetch: (request, { port }) => fetchSandboxPort(port, request), }, }, }, }), }); harness = new PiHarness({ harness: ({ storage, context, onWake }) => { const models = createModels(); models.setProvider(this.ai.provider); this.registry.install({ name: "browser", tools: [ browserTool({ browser: this.browser, loader: this.env.LOADER }), ], }); return Harness.open( storage, { models, registry: this.registry, onWake }, context, ); }, defaults: { model: this.ai("@cf/zai-org/glm-5.3-flash") }, }); lifecycle = Lifecycle.install(this).use(this.browser).use(this.harness); // InternalHostRouter calls this method to reach your Browser. fetchInternalHost(message: InternalHostMessage) { return this.browser.fetchInternalHost(message); } async ask(prompt: string) { const { text } = await this.harness.prompt(prompt); return text; }}
To use another harness, keep the browser configuration. Choose a browser-tool adapter if one is available, or implement a tool using browser.session().
browserTool tells the model about each host, including the schema of its target. Because preview.internal has a target schema, the model can choose which port it serves. To check its dev server, the model calls browser.assignHost() with a target that matches the schema, such as { port: 5173 }, and opens the URL it gets back:
The target is checked against the schema and stored, so it lasts across turns, browser restarts, and redeploys. Each request to http://preview.internal/ then calls the host's fetch function with { port: 5173 }.
This release also simplifies browserTool. It runs the model's code in its own Dynamic Worker and no longer needs a CodemodeRuntime export. Refer to the v0.31.0 upgrade notes ↗︎ if you already use browserTool.
For the complete example, limits, and security guidance, refer to Internal hostnames.
Challenge Pages served through the Cloudflare China Network now use Turnstile widgets hosted in Mainland China. This means that visitors in Mainland China can complete challenges faster and more reliably because widgets no longer need to make cross-border requests.
You do not need to take any action: this change applies automatically when a China Network zone presents a Challenge Page. Visitors served by Cloudflare's global network will continue to receive the global Challenge Page experience.
VPC Network bindings can select a specific virtual network by setting network_id to cf1:vnet:<UUID>, where the UUID is the ID of the virtual network. Continue to use cf1:network to select your account's default virtual network.
Multiple bindings can select different virtual networks. This lets one Worker reach separate backends that use the same private IP address:
Workers Static Assets can now serve assets from a URL path prefix without changing their directory structure. Set assets.base_path in Wrangler, or worker.assets.basePath in cloudflare.config.ts.
import { defineConfig } from "cf/config";export default defineConfig({worker: { name: "blog", // Set this to today's date compatibilityDate: "2026-10-29", assets: { basePath: "/blog", },},});
With Wrangler, assets.directory selects the directory to serve. With cloudflare.config.ts, your build configuration supplies the assets.
Workers Builds now retains each build and its build logs for 90 days after the build finishes. You can find them under View build history in your Worker's Deployments tab, or through the Workers Builds API.
After 90 days, a build and its logs are removed. The versions and deployments it created stay in your Worker's version history, so you can still roll back to them.
If you need a build's logs for longer, you can download them with the Get build logs API endpoint.
Cloudflare Access now supports HTML and Liquid templates for login, identity-denied, forbidden, and command-line approval pages. Custom templates allow you to control the branding and appearance of each supported page. Liquid variables allow you to provide custom instructions to users or provide a clear reason why they were blocked.
Create templates in the Cloudflare One dashboard, validate them in the editor, and assign them to applications or set account defaults.
For supported page types, template examples, and setup instructions, refer to Access custom pages.
The SQL API introspection endpoint has a new include_attributes query parameter. Setting it to true discovers the keys of a dataset's attributes JSON column, and their types, from the preceding seven days of your account's data. Discovered keys are returned in the dataset's attributes response field.
Some datasets store additional key-value data in an attributes JSON column, which you query with exact-key lookups such as attributes['http.method']. The previous way to discover these keys, include_custom_attributes, returned names in a custom_attributes field. The new names make it clearer that each returned name is a key you access through the attributes column.
include_custom_attributes and custom_attributes are deprecated but still work. Update integrations to use include_attributes and attributes.
You can now restrict a Workflow to a jurisdiction. Its instances only run and store data within that jurisdiction, which helps you comply with data localization regulations such as GDPR ↗︎ or FedRAMP ↗︎.
Set jurisdiction on a workflows binding, or on a Workflow declared in exports:
The supported jurisdictions are eu (the European Union), us (the United States), and fedramp (FedRAMP-Moderate data centers).
A Workflow's jurisdiction is set when the Workflow is created, and cannot be changed afterwards. If an existing Workflow has a different jurisdiction, wrangler deploy and wrangler versions upload stop before uploading your Worker.
Workflow jurisdictions require Wrangler 4.152.0 or above.
@cloudflare/computer ↗︎ gives your agent a workspace to run code in. From v0.7.0, that code can resume after a deploy or crash, and pause on an external action, for example to approve a tool call.
Resume interrupted runs
Set onInterrupt: "replay" on WorkerJavaScriptBackend, or on a single run.
import { DurableObject } from "cloudflare:workers";import { Workspace } from "@cloudflare/computer";import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";export class MyAgent extends DurableObject { workspace = new Workspace({ storage: this.ctx.storage, backends: [ new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", }), ], });}
import { DurableObject } from "cloudflare:workers";import { Workspace } from "@cloudflare/computer";import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";export class MyAgent extends DurableObject<Env> { workspace = new Workspace({ storage: this.ctx.storage, backends: [ new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", }), ], });}
When recovery is triggered, the JavaScript runs again from the beginning, reusing recorded results for completed calls without printing previously recorded output again. If a call's result was not recorded before the interruption, recovery may repeat the call — even if its action already succeeded. For retry handling, including unsafeToRepeat() for operations that cannot safely repeat, refer to Calls that must not run twice.
Recovery starts on the next call to workspace.runtime. To recover without waiting for traffic, call workspace.runtime.recover() from a Durable Object alarm.
Wait for approvals
Expose your application functions through custom modules. A function can call ctx.interrupt() to pause the run until your application supplies an approval decision.
Require approval before posting a comment
For example, to provide a GitHub module that allows an agent to post a comment on an issue with approval, an implementation of postComment could look like this:
new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", modules: { // The agent's code imports this with `import { postComment } from "ws:github"`. "ws:github": { postComment: async ([issue, body], ctx) => { const { approved } = await ctx.interrupt({ issue, body, }); if (!approved) throw new Error("The comment was rejected."); return github.comment(issue, body, { idempotencyKey: ctx.callId }); }, }, },});
new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", modules: { // The agent's code imports this with `import { postComment } from "ws:github"`. "ws:github": { postComment: async ([issue, body], ctx) => { const { approved } = await ctx.interrupt<{ approved: boolean }>({ issue, body, }); if (!approved) throw new Error("The comment was rejected."); return github.comment(issue, body, { idempotencyKey: ctx.callId }); }, }, },});
Call it from agent code
When the agent's code calls postComment(), the run pauses at ctx.interrupt() until your app responds. The agent's code calls it like any other function:
import { postComment } from "ws:github";export default async function ({ issue }) { return postComment(issue, "Hello world");}
Respond to the approval request
The time spent waiting does not count toward its timeout. The wait also survives deploys. Fetch the pending calls with workspace.runtime.pending() and resolve them with respond():
for (const call of await workspace.runtime.pending()) { await workspace.runtime.respond(call.id, call.callId, { approved: true });}
for (const call of await workspace.runtime.pending()) { await workspace.runtime.respond(call.id, call.callId, { approved: true });}
The run continues once all pending calls are resolved.
Starting today, the DNS settings endpoints gradually begin to represent Advanced Nameservers with nameservers.type: "cloudflare.advanced". The rollout is expected to take seven days. Previously, the API used the "foundation_dns": true setting for this configuration.
Before the rollout reaches an account, the endpoints continue to return nameservers.type: "cloudflare.standard" for Advanced Nameservers. The endpoints accept nameservers.type: "cloudflare.advanced" in PATCH requests for entitled accounts throughout the rollout. The zone settings JSON for /zones/{zone_id}/dns_settings is:
The foundation_dns boolean remains available as a deprecated compatibility alias in zone and account-default settings. Use nameservers.type: "cloudflare.advanced" for new writes.
You can continue to read and write foundation_dns during this transition. If a PATCH request includes both values, they must match. The API rejects conflicting values.
Update clients that treat nameservers.type as a closed enum to recognize the "cloudflare.advanced" value.
This API update does not change your Foundation DNS subscription or whether Advanced Nameservers are enabled on your zones. It does not require a zone migration.
In a future API change, Cloudflare will remove the foundation_dns parameter from the API endpoint. After migrating to nameservers.type, remove handling for the boolean from your client.
Custom spans in Workers now support three more methods from the OpenTelemetry span API.
span.spanContext() returns the span's traceId, spanId, and traceFlags. The IDs match the trace and span IDs in Workers Observability and in exported traces, so you can log them or return them to clients to correlate a request with its trace.
span.setStatus(status) sets the span status to "unset", "ok", or "error", with an optional message for errors. Span status is included in traces exported to OpenTelemetry destinations. The Workers Observability dashboard does not display it yet.
span.updateName(name) renames a span after it starts, such as once you know which route a request matched. You can also rename the invocation's root span returned by tracing.getActiveSpan().
This emergency release will add two rules related to the Brotli vulnerability CVE-2026-111497 to the Cloudflare Managed Ruleset. Both rules will be enabled by default with their action set to Log, providing visibility into matching requests so customers can evaluate the impact before choosing to block them.
Rule behavior
Brotli rule: Matches incoming requests that declare Brotli request-body encoding (Content-Encoding: br), including legitimate clients submitting Brotli-encoded request bodies. It does not match Accept-Encoding: br or affect Cloudflare's Brotli response-compression settings. The rule's default action is Log.
Next.js rule: Matches requests to the Next.js image optimization endpoint (/_next/image), including legitimate image optimization traffic. The default action is Log and does not block requests. Changing the action to Block may prevent optimized images from loading and disrupt image delivery for applications using this endpoint.
Default behavior
Both rules will log matching requests without blocking them when the ruleset defaults are used.
Customers will be able to review matches in analytics and configure a blocking action for either rule if desired.
Enabling these rules in Log mode provides visibility only; it does not block requests matching these rules.
Important: Check existing action overrides
If you have configured a ruleset-level action override that applies Block to these rules, matching requests may be blocked as soon as the rules are deployed, despite their default Log action. This may result in false positives and block legitimate traffic.
Review your existing ruleset overrides before deployment. If you want to evaluate these rules without blocking traffic, configure rule-specific overrides to use Log and review matching requests before switching to Block.
The D1 undelete API now only accepts requests for databases that were deleted within the last 7 days.
Requests to undelete a database that was deleted more than 7 days ago will fail and return an error.
If you delete a D1 database by mistake, send the undelete request as soon as possible, and no later than 7 days after deletion.
Flagship supports nested objects, arrays, and null in evaluation context. Pass user profiles and tag lists directly, then target nested attributes without flattening your application data.
For example, pass a profile and tags when evaluating a flag through the Workers binding:
A targeting rule can combine profile.plan equals "enterprise" with tags has "beta" to serve the feature to enterprise users in the beta group. Use dot-separated paths for nested attributes and the has and not_has operators for array membership. Percentage rollouts can also use a nested identifier, such as profile.id, for consistent user assignment.
Structured context works through the Workers binding, the evaluation REST API with a JSON POST body, and the TypeScript, Python, and Go OpenFeature SDKs from version 0.6.0. The SDKs automatically use POST when HTTP evaluation requires structured context.
The Cloudflare Radar homepage has been redesigned to make global Internet data easier to explore for broader audiences, including journalists, policymakers, and everyday users, while preserving the depth that technical users rely on.
Interactive map: Shows Internet traffic and outages around the world, and acts as a starting point for exploring specific countries and trends.
Curated insights section: Tabbed navigation that highlights summary statistics across Radar data, with a call to action to explore each area in depth.
Updated interface: Built with Kumo ↗︎ components, the same ones used across Cloudflare products.
As we continue the redesign, all Radar pages will see improvements. Share feedback at radar@cloudflare.com.
The private keys of eight eMudhra intermediate certificate authorities ↗︎ can be recovered, so anyone can forge a certificate for any hostname that chains to them. The Workers Builds and Pages build images now reject these certificates when your build clones your repository, installs dependencies, or makes HTTPS requests.
The updated images include patched CA certificates, Node.js 22.23.4 and 24.22.1, Bun 1.4.4, and pip 26.2.2. Build commands also run with environment variables that point older runtimes at the patched CA certificates.
What you need to do
Most projects need no changes. Update your project if:
You set BUN_VERSION below 1.4.4. Older Bun versions trust the compromised certificates.
You set your own SSL_CERT_FILE, PIP_CERT, or REQUESTS_CA_BUNDLE, or set --use-bundled-ca in NODE_OPTIONS. Make sure your CA certificates do not trust the compromised intermediates.
Your build installs tools that ship their own CA certificates. Update them to a version that removes the compromised intermediates.
For details, refer to the build image documentation for Workers Builds and Pages.
You can now choose the npm version that Workers Builds uses by setting the packageManager field in your package.json. Before the build installs dependencies, it installs the npm version you set, so a lockfile written by a newer npm version installs the same way in your build as it does locally.
package.jsonjson
{ "packageManager": "npm@11.19.1"}
The packageManager field also sets the version of Yarn, pnpm, and Bun. Use an exact version, such as npm@11.19.1, not a range. Workers Builds does not use the engines.npm field to choose an npm version.
Added automatic endpoint selection for Global Acceleration. When Global Acceleration is enabled for an organization, the client connects to the Global Acceleration endpoints while the device is in the acceleration region and to the standard anycast endpoints otherwise, and reconnects when a network change moves the device between them without requiring action from the user. The Connectivity page shows which tunnel and API endpoints are in use, and users who are allowed to override the selection can choose endpoints in Settings.
Added protection against unauthorized modification or uninstallation of the Cloudflare One Client. When enabled, the user will be asked to provide an administrator-generated code when trying to uninstall the client, which will otherwise be blocked.
The client now shows a banner when the network limits the tunnel MTU to a degraded or critically low value. The banner clears when the MTU recovers or the client disconnects.
Additional Changes and improvements
The client now applies tunnel MTUs between 1147 and 1199 bytes measured by Path MTU Discovery. Previously the client ignored these values and kept a larger MTU, which could drop larger packets on networks such as mobile hotspots while the client showed as connected.
When Path MTU Discovery is enabled, the client now accepts larger packets from Cloudflare instead of limiting them to the size it uses for its own outbound packets.
Access reauthentication requests now trust certificates in the operating system's certificate store, so reauthentication works on networks that perform TLS inspection.
For Zero Trust deployments, the client now records a traceroute of the network path outside the tunnel when the tunnel degrades or fails to connect. warp-diag includes the same trace.
warp-diag now includes redacted copies of the deployed MDM files and the names, but not the values, of stored registration secrets. It reports a QUIC path MTU only when one was measured.
Added the MDM parameters mdm_file_debounce_secs (1–60) and mdm_file_unavailable_debounce_secs (0–300), which set how long the client waits before applying a changed MDM file and before acting on a missing or unreadable one.
The consumer client home screen now shows when the connection is post-quantum encrypted.
The client UI no longer starts WMIC or PowerShell processes to collect memory information for crash reports.
Fixed a hostname-based split tunnel entry not taking effect until the next reconnect after its firewall rule failed to apply.
Fixed the client window appearing blank or white after an upgrade, at sign-in, after waking from sleep, or after a display change.
Fixed the client service restarting when a firewall update for captive portal detection stalled while the device was asleep.
Fixed the client getting stuck in a disconnected state on Windows on Arm devices because leftover tunnel adapters were not removed.
Fixed the client being unable to connect after an upgrade, downgrade, and upgrade that switched between consumer and Zero Trust registrations. The client now clears the mismatched registration so the user can register again.
Fixed the client UI looking for one of its libraries in a directory that standard users can create before looking in the installation directory.
Fixed warp-cli registration license reporting an error on slow networks even though the license was applied, which left the client registered to a WARP+ account it did not recognize.
Fixed Digital Experience Monitoring (DEX) results being reported with timestamps offset from the actual time after the system clock was corrected.
Added the MDM parameters managed_network_probe_interval_secs (30–300, default 30) and managed_network_probe_count (0 disables probing, 1–10, or -1 to keep probing until a managed network is found; default 1). After a registration or network change finds no managed network, the client now rechecks, so a device doesn't stay on the default profile because of a transient connection failure. Set the count to 0 to keep the previous behavior.
In proxy mode for Zero Trust, the client now sends hostnames from HTTP/HTTPS and SOCKS5 requests to Cloudflare for resolution instead of resolving them on the device.
When the app cannot reach the client service, the home screen now says why (service not running, not accepting connections, not responding, or connection dropped and reconnecting). It shows an error code and a Learn more link instead of "IPC error".
Fixed Windows resetting the Wi-Fi adapter after sign-in or a Wi-Fi restart when the client's firewall rules in Exclude split tunnel mode blocked Windows network connectivity checks. The client now allows those checks.
Fixed the installer asking the user to close running applications during an upgrade, downgrade or uninstall because other programs loaded the client's Visual C++ runtime libraries through the system PATH. The client now loads these libraries from a private folder so that closing other applications is unnecessary.
Known issues
A Windows DNS client regression may cause connectivity check failures on systems containing large hosts files. While our client includes a mitigation for this issue, users may still experience reduced DNS performance and connectivity check failures.
macOS 15.0 is now the minimum supported version. This release does not run on macOS 14.
Added automatic endpoint selection for Global Acceleration. When Global Acceleration is enabled for an organization, the client connects to the Global Acceleration endpoints while the device is in the acceleration region and to the standard anycast endpoints otherwise, and reconnects when a network change moves the device between them without requiring action from the user.. The Connectivity page shows which tunnel and API endpoints are in use, and users who are allowed to override the selection can choose endpoints in Settings.
The client now shows a banner when the network limits the tunnel MTU to a degraded or critically low value. The banner clears when the MTU recovers or the client disconnects.
Additional Changes and improvements
The client now applies tunnel MTUs between 1147 and 1199 bytes measured by Path MTU Discovery. Previously the client ignored these values and kept a larger MTU, which could drop larger packets on networks such as mobile hotspots while the client showed as connected.
When Path MTU Discovery is enabled, the client now accepts larger packets from Cloudflare instead of limiting them to the size it uses for its own outbound packets.
Access reauthentication requests now trust certificates in the operating system's certificate store, so reauthentication works on networks that perform TLS inspection.
For Zero Trust deployments, the client now records a traceroute of the network path outside the tunnel when the tunnel degrades or fails to connect. warp-diag includes the same trace.
warp-diag now includes redacted copies of the deployed MDM files and reports a QUIC path MTU only when one was measured.
The consumer client home screen now shows when the connection is post-quantum encrypted.
Fixed a managed registration being removed when the MDM configuration file was briefly missing or unreadable. The client now keeps the last applied policy for 20 seconds after the file becomes unavailable. The new MDM parameters mdm_file_debounce_secs (1–60) and mdm_file_unavailable_debounce_secs (0–300) change how long the client waits before applying a changed file and before acting on a missing one.
Fixed a hostname-based split tunnel entry not taking effect until the next reconnect after its firewall rule failed to apply.
Fixed split tunnel updates for hostname-based entries failing when the client tried to remove a route that macOS had not installed.
Fixed the client UI not showing onboarding on first launch while a notification permission request was pending.
Fixed warp-cli registration license reporting an error on slow networks even though the license was applied, which left the client registered to a WARP+ account it did not recognize.
Fixed Digital Experience Monitoring (DEX) results being reported with timestamps offset from the actual time after the system clock was corrected.
Fixed an upgrade sometimes leaving the client without a running service when the previous service was slow to stop. The installer now waits up to 10 seconds for the old service to be removed before starting the new one.
Fixed the OS version detail stored for a device keeping an outdated value, including the text "Usage: sw_vers…" reported by client versions 2023.5.587.1 through 2024.12.760.0. The client now clears the value the next time it connects.
Added the MDM parameters managed_network_probe_interval_secs (30–300, default 30) and managed_network_probe_count (0 disables probing, 1–10, or -1 to keep probing until a managed network is found; default 1). After a registration or network change finds no managed network, the client now rechecks, so a device doesn't stay on the default profile because of a transient connection failure. Set the count to 0 to keep the previous behavior.
In proxy mode for Zero Trust, the client now sends hostnames from HTTP/HTTPS and SOCKS5 requests to Cloudflare for resolution, instead of resolving them on the device.
When the app cannot reach the client service, the home screen now says why (service not running, not accepting connections, not responding, or connection dropped and reconnecting). It shows an error code and a Learn more link instead of "IPC error".
Cloudflare's product-specific MCP servers now serve only /mcp. A request to /sse returns 410 Gone with the server's /mcp URL, which most MCP clients show in their connection error.
If your MCP client is configured with an /sse URL, change it to /mcp, for example https://docs.mcp.cloudflare.com/mcp. If you signed in through /sse, your client asks you to sign in again. Connections that already use /mcp are not affected.
Agents SDK v0.30.0 runs PiHarness on Pi 1.4 ↗︎, and makes it recover more reliably from evictions, crashes, and storage errors.
Hibernation while Pi's tasks sleep.PiHarness passes Pi's new onWake callback to your harness factory. While a task sleeps, such as during a model retry backoff, the Durable Object can hibernate until the task is due instead of staying in memory.
Parked tasks. Pi 1.4 parks a task that keeps crashing the object instead of restarting it forever. Parked operations show as parked in pending(), and session.retry() runs them again.
Recovery from storage errors.PiHarness reopens Pi after a storage error instead of failing every call until the object restarts. Its wake-up job retries failed steps with a backoff instead of being dropped.
Retried model streams.agents/models/pi-ai reports a cut-off or idle model stream as a transient error, so Pi retries the turn instead of failing it.
Upgrade
The Agents SDK v0.30.0 changelog ↗︎ has step-by-step upgrade instructions. Point your coding agent at them and upgrade today:
Pi 1.4 migrates the session storage in the object's SQLite database to a schema that Pi 1.3 cannot open, so a deployment cannot roll back past this upgrade. Refer to the Pi Durable 1.4.0 changelog ↗︎ for Pi's own breaking changes.
The private keys of eight eMudhra intermediate certificate authorities ↗︎ can be recovered, so anyone can forge a certificate for any hostname that chains to them. Sandbox SDK images before 0.12.14 trust these certificates. An HTTPS connection that leaves the container directly, such as one from mountBucket(), backups, gitCheckout(), tunnels, or code that runs in the sandbox, can be intercepted with a forged certificate.
What you need to do
Sandbox SDK 0.x
Update immediately. Install @cloudflare/sandbox@0.12.14, change the FROM line in your Dockerfile to the matching image, and redeploy. Keep any -python, -opencode, or -musl suffix on the image tag. The deployment's rollout replaces running sandboxes with ones that use the new image.
Dockerfiledockerfile
FROM docker.io/cloudflare/sandbox:0.12.14
If you use the next prerelease, update to 0.13.0-next.825.1 or later.
If you cannot update yet and use 0.8.7 or later, send all HTTPS through your Worker, so that Cloudflare checks every certificate on port 443 whatever your image trusts:
Deploy, then stop running sandboxes so that they restart with the interception certificate. This does not cover TLS on other ports, such as cloudflared tunnels, and clients that ignore the injected certificate fail to connect. For more information, refer to HTTPS traffic.
Sandbox SDK 1.x
You do not need to update the SDK, because it makes no TLS connections of its own. Your image decides which certificates your sandboxes trust, so rebuild it and replace the sandboxes that run the old one:
Run docker pull for each base image in your Dockerfile, then clear the Docker build cache with docker builder prune --all. wrangler deploy builds with your local images and build cache, so without these steps it can ship the old CA certificates and runtimes. If your Wrangler configuration references an image by digest, build and push a new image and update the digest instead.
Deploy your Worker.
Stop sandboxes that are still running, for example with this.ctx.container.destroy(). A running sandbox keeps the image it started with until it stops. To restart only the sandboxes whose image is out of date, refer to Roll out a named image update.
If you use snapshots, create new ones from sandboxes that run the updated image. A snapshot restores the image it was created from, so an older snapshot brings back the old certificates.
Check your sandboxes
From inside a sandbox, connect to one of the test hosts listed at emudhra.we-have-a-problem.com ↗︎ with each TLS client your code uses, such as curl, node, or bun. Every request should fail:
In the 0.12.13 images, the system CA bundle, Node.js, cloudflared, and pip reject certificates from the compromised intermediates. The images ship updated Ubuntu and Alpine CA certificates, Node.js 22.23.4, cloudflared 2026.10.2, and pip 26.2.2 in the -python image.
0.12.14 updates Bun to 1.4.4, so the bun command and createBackup() uploads of archives of 10 MiB or more reject these certificates too. The -opencode image ships OpenCode 1.19.4 or later, which also runs on Bun 1.4.4.
Starting from version 2026.10.0, Cloudflare Tunnel can normalize URLs before matching ingress rules. This improves security for tunnels with rules that require Cloudflare Access validation. The URL path sent to your origin stays the same.
In three months, starting 2027-01-25, new cloudflared releases will turn on URL normalization by default. Test your path-based routing before that date, as this change may affect which ingress rule matches a request. Configurations without rules requiring Access validation are unaffected.
This setting is currently off by default. To turn it on, run:
cloudflared tunnel --disable-path-normalization=false run <TUNNEL_NAME_OR_UUID>
Alternatively, set TUNNEL_DISABLE_PATH_NORMALIZATION=false on each cloudflared replica and restart it.