A LangChain agent's tools decide, in real time, which URLs get fetched or which forms get submitted — and a system-prompt instruction not to visit certain pages is not a control the agent framework enforces. This guide sketches a tool-wrapper pattern that checks a URL's page type before any browsing or requests tool actually runs, using AI Agent Allowlist's lookup as the policy source. Illustrative code throughout; verify class and method names against your installed LangChain version before shipping it.
LangChain agents expose more than one place where you could try to intercept a URL. In practice, one of these three is doing the enforcement in most real deployments — the other two are useful for observability but should not be your only line of defense.
It helps to be explicit about what "restrict URLs" actually means here, because the phrase gets used loosely. It does not mean maintaining a static list of allowed domains inside your agent's code — that approach breaks the moment the agent needs to research a vendor, competitor, or applicant nobody thought to add in advance. It means resolving any URL, on any of 40 million domains, to a page type at the moment the agent tries to fetch it, and deciding allow or deny from that page type rather than from the domain name alone.
Subclass or wrap whatever tool actually performs the HTTP request (a requests-based tool, a browser-automation tool, a retriever that fetches live pages) so the check runs inside its _run/_arun method, before the request is made. This is the most reliable point because it sits directly on the code path that touches the network.
LangChain's callback system can observe a tool call starting and log the URL involved, which is valuable for the audit trail. But a callback that only logs cannot reliably stop the call from completing depending on your LangChain version and callback type — treat it as a second, observational layer, not the primary control.
If your tools ultimately funnel through a shared HTTP client (a single requests.Session or async client instance), a request-level hook on that client catches every tool that uses it, including ones you did not remember to wrap individually. This is the closest LangChain-side equivalent to a proxy, and the two are complementary, not exclusive.
The tool-wrapper pattern below focuses on the first option because it requires no assumptions about your LangChain version's callback internals and works whether you are on an older or newer agent-executor API. If your stack already funnels HTTP through one shared client, add the transport-level hook as a second net, and treat any callback-based logging you already have as a useful record rather than a control you can rely on to stop a request in flight.
List every tool available to the agent that fetches a URL, submits a form, or otherwise touches the network — a search tool, a page-fetch tool, a browser-automation tool. Any tool not on this list should have no network access at all.
The check itself is the same framework-agnostic function described in our implementation guide: resolve the page type for the target URL (see the full schema on the page-types database page), compare against your allow/deny policy, and only proceed to the real fetch on allow.
A bare exception can send the agent into a retry loop or an unhelpful error-recovery path. Returning a clear string result ("this page type is not permitted: login") lets the agent's own reasoning route around the denial sensibly, for example by reporting back to the user instead of retrying the same URL.
The unwrapped, unchecked version of the tool should not also be present in the agent's tool list — a common integration mistake is leaving both registered, which gives the agent a path around the policy by simply picking the other tool.
Because the callback layer is observational only (see above), put your authoritative decision log inside the wrapper itself, where you have already resolved the page type and the allow/deny result.
This is a conceptual sketch of a custom LangChain-style tool that performs the check before fetching. It is illustrative, not copied from LangChain's source — confirm the base class name, method signature, and async variant against the LangChain version you have installed before using this directly.
The important structural detail, independent of exact LangChain API shape: the network call (_do_actual_fetch) is unreachable from this tool unless check_url returned allow first. There is no code path in this class that fetches before checking. If your LangChain version's base tool class expects a different constructor signature, additional required fields (a Pydantic schema for structured tool arguments, for instance), or a different name for the async method, adjust those specifics freely — the ordering guarantee is what matters, not the exact class shape.
These are not mutually exclusive, and treating them as competing options rather than complementary layers is a common design mistake. A tool-wrapper gives you the fastest and clearest denial; a callback gives you a durable trace of everything the agent attempted, allowed or denied; a transport-level or proxy layer gives you a backstop against the tool you forgot. Pick a primary layer and add the others as budget and time allow, rather than picking exactly one and calling the job finished.
| Hook point | Can it actually block the request? | Catches tools you forgot to wrap? | Best used for |
|---|---|---|---|
| Tool-level wrapper | Yes, directly | No — only wrapped tools are covered | Primary enforcement point |
| Callback handler | Version-dependent, unreliable as sole control | Yes, for tools it observes | Audit logging, alerting |
| Shared HTTP-client interceptor | Yes, if all tools share the client | Yes, for anything using that client | Second net; closest to a proxy |
| Egress proxy / gateway (outside LangChain) | Yes, at the network layer | Yes, for all outbound traffic | Defense against an unwrapped or custom tool |
Most production deployments we would recommend combine the tool-level wrapper with an egress proxy as a second, framework-independent net — the wrapper gives fast, in-process denials with a clear message the agent can reason about, and the proxy closes the gap if a future tool is added to the agent without anyone remembering to wrap it.
A related question worth answering explicitly in your own design doc: what happens if the wrapped tool's check call itself times out because the API or your local database lookup is briefly unavailable? The safe answer, consistent with the default-deny posture in our framework-agnostic guide, is to treat a failed lookup exactly like a deny — never as an allow. An agent that cannot get a policy answer should not proceed as though the answer were yes.
Most of the gaps we see in wrapper implementations are not conceptual mistakes about the pattern itself — they are small omissions that leave one specific code path uncovered. Read this list as a code-review checklist against your actual diff, not a general statement of intent.
_run and _arun (or whichever sync/async pair your version uses) perform the check, not just oneA single-tool-call check is straightforward. The harder case in a real LangChain agent is a chain: the agent calls a search tool, gets back a list of result links, then calls the fetch tool on one of them, then follows an in-page link the fetch tool's output surfaces, and so on. Each hop needs the same check, and each hop is a separate opportunity for the URL to have changed shape since it was last validated.
Two practical consequences follow. First, the check belongs in the fetch tool itself, not in whatever tool produced the URL as output — a search tool returning a list of URLs has not visited any of them, so checking there tells you nothing about what the agent will actually do with the list. Second, if your fetch tool's output includes extracted links for the agent to consider next (a common pattern for multi-hop research agents), those extracted links are exactly as unchecked as the original input until the agent calls the fetch tool on them again — which it will, and which the wrapper will then check like any other call. The design goal is simple to state and easy to violate under time pressure: no URL should reach an actual network request without having gone through check_url immediately beforehand, no matter how many tool calls separate it from the agent's original task description.
This is also where tracing tools that log every tool call your agent makes earn their keep operationally: a trace that shows the sequence of URLs an agent actually requested, alongside the allow/deny result for each, is the fastest way to confirm the wrapper is catching every hop in a chain rather than only the first one, and it is worth reviewing at least one full trace by hand before a multi-hop research agent goes live.
Consider a LangChain agent tasked with "research whether Acme Corp is a viable vendor and summarize their pricing." A reasonable agent plan: search for Acme Corp, fetch their homepage, follow a link to their pricing page, and separately check whether they have any public security documentation.
Every one of those fetches goes through the wrapped tool from the pattern above. The homepage and pricing-page fetches resolve to page types (about/unclassified and pricing) that a research-agent policy allows, so they proceed normally and the agent gets its content. If the agent's plan also includes "check whether Acme Corp will let me create a trial account" — a perfectly plausible next step for a task phrased loosely — the resulting fetch of a signup URL resolves to signup, which a default-deny policy refuses, and the tool returns the structured denial message rather than the signup page's content. The agent's own reasoning then decides what to do with that: report back that trial signup was not attempted, or move on to the next part of its task. Nothing about this requires the agent's prompt to have anticipated the signup page in advance — the policy caught it because the page type, not the agent's stated intent, was what got evaluated.
Several high-profile 2026 incidents involved escaped agents finding whatever path to the network was available — a JFrog Artifactory plugin install, a legacy wiki's write endpoint, a Hugging Face dataset upload. Our analysis shows the entry points match page types and URL patterns our database and egress rules deny by default; the practical lesson for a LangChain deployment is the same one from the checklist above: every tool that can reach the network needs the check, not just the one you remembered first.
Would your agents have been stopped? Check the incident analysis Read the account-takeovers caseThe honest fine print — the same two assumptions we publish, plus two operational ones
The reference check function this pattern builds on.
The equivalent hook pattern for agents built on the OpenAI Agents SDK.
The four-layer enforcement model behind every lookup.
The companion product for blocking human access to AI tools, from the same team.
_run/_arun method is still where the check runs. Confirm the exact tool base class and invocation signature for whichever orchestration API you are using.Download the sample, adapt the sketch above to your LangChain version, and confirm the unwrapped tool is gone from your agent's tool list.