How to stop AI agents contacting malicious URLs and domains

Check every host that an agent is about to contact before the tool call runs, score each one against threat intelligence, and deny the call when any host scores high or critical risk. Arcjet denies a call to a high-risk host before the connection opens. On Claude Code, GitHub Copilot, Cursor, and OpenAI Codex, one policy runs from the agents' own hooks and scores URL arguments and absolute URLs in shell commands. In custom agents, the same check runs inside the tool. It fails closed when a lookup can't complete.

11 min read
In short: Check every host that an agent is about to contact before the tool call runs, score each one against threat intelligence, and deny the call when any host scores high or critical risk. Arcjet denies a call to a high-risk host before the connection opens. On Claude Code, GitHub Copilot, Cursor, and OpenAI Codex, one policy runs from the agents' own hooks and scores URL arguments and absolute URLs in shell commands. In custom agents, the same check runs inside the tool. It fails closed when a lookup can't complete.

How do you stop an AI agent contacting malicious URLs and domains?

Check every host that an agent is about to contact before the tool call runs, score each host against threat intelligence, and deny the call when any host scores high or critical risk. The check belongs on the tool call itself, because that is the last point before the connection opens. A filter on the model's output or a line in the system prompt runs too early or not at all.

This is outbound threat detection, the reverse of the usual direction. Web application security scores the client that connects to you. An agent is a client too: it fetches pages, calls APIs, runs curl, and talks to Model Context Protocol (MCP) servers, which expose tools to the agent. For an agent, the question is whether the host it is about to reach is known to be malicious.

Arcjet does this with destination threat analysis. On coding agents such as Claude Code, GitHub Copilot, Cursor, and OpenAI Codex, Arcjet decides each tool call from the hooks that the agent already fires, with one policy across all four and no code change. In custom agents, Arcjet runs the same check inside the tool before it contacts anything. In both cases, a call to a high-risk host is denied before the connection opens.

Why do AI agents contact hosts nobody reviewed?

A person who pastes a URL into a terminal has usually looked at it. An agent picks its destinations from its context, and much of that context comes from sources that nobody on your team wrote. Most unreviewed destinations arrive by one of the following routes:

  • Tool arguments. The model fills in a URL for a fetch tool, an API client, or a shell command. It chooses the host from whatever it read last.
  • Fetched content. A web page, a README, a package's install instructions, or an MCP tool's response names a URL, and the agent follows it.
  • Injected instructions. Text planted in an issue, a document, or a page tells the agent to download a file or send data to a host the attacker controls. For more information about this route, see indirect prompt injection in agentic workflows.

None of these routes require the model to be jailbroken. The agent is acting on what it reads, which is what it was built to do. A check on the destination at run time holds up where instructions that tell the agent to be careful don't. For more information about why prompt-level rules don't hold, see AGENTS.md is not a security control.

What is destination threat scoring?

Destination threat scoring looks up each host that a call would contact in a threat intelligence database and returns a risk level for it. A policy then decides whether that risk level is acceptable for the action.

In Arcjet, a policy declares an Arcjet threat intelligence detector over a list of hosts. The detector scores those hosts with the same Arcjet threat intelligence that scores the caller of an inbound HTTP request, and the lookup runs only when a published policy declares it. The policy is written in Rego and reads the detector's verdict under input.signals.ip_threat, keyed by the detector ID that you declared. Evaluation runs at the edge in over 300 data centers, so the added latency on each tool call stays small.

Which hosts does destination scoring check?

The detector scores a list of hosts. On a coding agent, Arcjet fills that list for you in an input named destinations, which holds the following kinds of host:

  • The hosts of the tool call's URL arguments, such as the URL a WebFetch call is about to open.
  • The hosts of absolute URLs inside a shell command, such as the URL in curl https://example.com/install.sh.

Arcjet scores shell commands as well as web tools, because an agent with a shell can reach any host with curl or wget. A rule that looked only at web tools would miss that path.

Arcjet skips private, loopback, link-local, carrier-grade NAT (CGNAT), multicast, and unspecified addresses, so calls to internal services aren't scored. An empty list scores risk none. A call with more than eight public destinations fails closed, which means Arcjet denies it rather than deciding on partial information. Scoring the first eight and deciding anyway would let a malicious host through past the cut.

What does a destination verdict contain?

The detector rolls up the result across every public host that completed scoring, and the worst host wins. The following table lists the fields a policy can read:

FieldMeaning
detectedtrue when risk_level is high or critical
risk_levelnone, low, medium, high, or critical
reputationUpstream label such as malicious, suspicious, or unknown
activitiesObserved behaviors for the worst host
host

Host that produced the worst assessment, empty when risk is none

ipAddress looked up for that host
assessments

One entry per public destination that completed scoring, in input order

Each entry in assessments carries its own host, ip, risk_level, reputation, activities, and detected fields. With that per-host list, one policy can treat a reviewed host differently from an unreviewed one. For more information, see how to build an egress allowlist for AI agents.

How do you write a policy that denies high-risk destinations?

Deny the call when the rolled-up risk level is high or critical. Arcjet ships the following policy as the coding-agent.destination-threat starter in the Arcjet Console. It declares the inputs tool_kind and destinations, runs a detector with the ID dest over destinations, and denies for any tool kind:

package arcjet.guard
import rego.v1
deny contains "malicious-destination" if {
input.signals.ip_threat.dest.risk_level in {"high", "critical"}
}

Name the set of risk levels you deny. A string comparison such as >= "high" is wrong, because critical sorts before high and none sorts after it.

The rule doesn't check tool_kind, so it applies to a shell command as well as a web fetch. The following table shows what the starter decides:

SituationResult
Empty destinationsAllowed (risk none)
Host with no threat recordAllowed (risk none)
Low riskAllowed
High or critical riskDenied (malicious-destination)

On a coding agent, set Execute on to Tool call in the Arcjet Console. Don't choose Prompt: destinations is empty when a prompt is submitted, so a live detector rule with nothing to score fails closed and denies every prompt. A live rule is one that enforces, as opposed to a dry-run rule, which records what it would deny without blocking anything. The agent receives only the rule ID as the denial reason, for example malicious-destination.

What happens when a lookup fails?

A check that fails open, allowing the call whenever it can't finish, is easy to defeat. Arcjet destination scoring fails closed under the following conditions, where it can't produce an answer:

ConditionResult
Host missing from the threat databaseRisk none
Lookup or DNS resolution can't complete

The policy can't finish evaluating, so a live rule denies (AJP1009)

One host already high or critical while another lookup failsHigh or critical verdict is still returned
More than eight public destinationsFails closed

Deployment can't reach the IP database when a destination is present

Fails closed

A host whose lookup failed is left out of assessments. In that case the detector either fails closed or already has a high or critical host that decides the call.

Which controls pair with destination scoring?

Threat intelligence tells you whether a host is known to be high risk. A host with no record scores none and is allowed. A newly registered domain, a legitimate file-sharing site, or a public paste service can all score low and still be somewhere that you don't want your data to go. Pair scoring with the following controls:

  • An egress allowlist. When you know which hosts an agent needs, list them, and deny a call to any host outside the list. The allowlist catches the unknown host that threat intelligence has no record of, and Arcjet runs both checks in one policy. For the combined policy, see how to build an egress allowlist for AI agents.
  • Package scanning. Scoring covers the hosts that a call names. A command such as npm install some-package names no URL, and the package comes from a registry with a clean reputation. For more information about package trust, see how to stop a coding agent downloading and running malware.
  • A reliable hook path. Claude Code and Copilot HTTP hooks fail open on a timeout or network error, which means the call goes ahead. Arcjet keeps the added latency small by evaluating at the edge, and the Arcjet wrapper for Codex and Cursor fails closed. For more information, see coding agent hooks fail open.

How does this work for coding agents and custom agents?

With Arcjet, a tool call to a known-malicious host is denied before the connection opens, whether the agent is one your developers use or one you build.

For coding agents, Arcjet uses coding agent hooks. A hook is a request that the agent sends to an external service before it runs a tool, and the service's answer decides whether the tool runs. Arcjet reads the hook that each agent already fires, so one policy covers Claude Code, Copilot, Cursor, and Codex with no SDK and no code change. You install the hooks through managed settings or mobile device management (MDM), so a developer can't remove them without administrator access. For more information, see the coding agent security overview.

For custom agents you build with frameworks such as LangChain, the OpenAI Agents SDK, or the Vercel AI SDK, Arcjet runs the same check inside the tool before it contacts anything. Map the hosts that the tool is about to contact into a declared SERVER string list input, declare an Arcjet threat intelligence detector over that input, and use the same Rego in your guard() call. That works for a fetch tool, an outbound API client, or a connection to a remote MCP server. For more information about agent tools that call external services, see runtime controls for AI agents calling external APIs.

Arcjet records every decision in the Arcjet Console and exports decisions to Datadog, Splunk, SentinelOne, Panther, and Amazon S3 (Enterprise plan), so a denied destination reaches your detection and alerting. Policy changes take effect in real time.

To start on a coding agent, publish the coding-agent.destination-threat starter in dry run. Read the calls that it would have denied in the Activity view of the Arcjet Console, and then set the rule live.

Learn more: Destination threat detection · Coding agent policies · Policy examples

Frequently asked questions

How do I stop an AI agent from visiting malicious websites?

Check each host on the tool call before the call runs. Score the hosts with threat intelligence and deny the call when any host is high or critical risk. Instructions in the system prompt aren't enough, because the agent picks URLs from content that it reads, including injected content. Arcjet runs this check before the tool runs: for coding agents from the hooks they already fire, with one policy across vendors, and for custom agents inside the tool before it contacts anything.

Does destination scoring cover curl commands, or only web fetches?

On a coding agent, Arcjet scores the hosts of URL arguments and the hosts of absolute URLs inside a shell command, so a curl or wget to a high-risk host is denied. A command with no URL in it, such as a package install, has nothing to score.

What happens if the threat lookup fails?

Destination threat scoring fails closed. When a lookup or DNS resolution can't complete, the policy can't finish evaluating, and a live rule denies the call. A call with more than eight public destinations is also denied. A host that isn't in the threat database scores risk none and is allowed.

Are private or internal addresses scored?

No. Arcjet doesn't score private, loopback, link-local, carrier-grade NAT (CGNAT), multicast, or unspecified addresses, so a call to an internal service produces risk none.

Is threat intelligence enough to stop data exfiltration by an agent?

No. Threat intelligence catches hosts known to be high risk. A new domain or a public upload site can score low, so pair scoring with an egress allowlist of the hosts the agent needs. Arcjet runs both checks in one policy.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Publish the destination-threat starter policy in dry run and see which hosts your coding agents would have been refused.