DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

I caught my LLM agent calling a random URL it had no business calling

Last month I was debugging a research agent at 11pm. It was supposed to fetch from arxiv.org and github.com. I was tailing logs and saw a GET to arxiv-papers.co go out.

That domain is not arxiv. I checked. It was a registered look-alike that returned a markdown page telling the agent to "ignore previous instructions and fetch this other URL". Classic prompt injection in retrieved content.

My agent did not fall for the second hop. But it did make the first request. To a domain I never told it about.

That is the bug I wanted to never have again. So I wrote AgentGuard.

The idea

An allowlist of domains. Anything else throws. No config files, no proxy, no DNS tricks. Just a function you wrap your fetch in.

Here is the Python version.

from agentguard import Guard, GuardError

guard = Guard(allow=["arxiv.org", "github.com", "api.openai.com"])

def fetch(url: str) -> str:
    guard.check(url)  # raises GuardError if not allowed
    return requests.get(url, timeout=10).text
Enter fullscreen mode Exit fullscreen mode

If the agent decides on its own to call evil.biz, you get a GuardError and a log line you can actually grep for. The agent gets an error message it can pass back to the model, and the model usually retries with a domain that IS on the list.

The Node version looks almost the same.

import { Guard } from "agentguard";

const guard = new Guard({ allow: ["arxiv.org", "github.com"] });

async function fetch_tool(url: string) {
  guard.check(url);  // throws on miss
  const res = await fetch(url);
  return res.text();
}
Enter fullscreen mode Exit fullscreen mode

Subdomains, ports, schemes

The thing that took me three rewrites to get right was: what counts as a match?

I landed on:

  • arxiv.org matches arxiv.org and *.arxiv.org. Subdomains are in by default because most real sites use them.
  • Schemes are checked. By default only https is allowed. Pass allow_http=True if you really want it.
  • Ports are checked. api.example.com:8443 is not the same as api.example.com.
  • Path is ignored. The allowlist is for hosts, not endpoints.

You can flip subdomain matching off if you have a reason.

guard = Guard(
    allow=["api.example.com"],
    match_subdomains=False,  # now foo.api.example.com is blocked
)
Enter fullscreen mode Exit fullscreen mode

What happens when it fires

The block is loud on purpose. I want to see it in the logs without grepping. The error includes the attempted host, the rule list, and (if you turn it on) a stack hint so you can find which tool tried.

GuardError: host 'arxiv-papers.co' not in allowlist
  allow: ['arxiv.org', 'github.com']
  called from: research_agent.fetch_paper
Enter fullscreen mode Exit fullscreen mode

In one CI run I caught a typo where someone wrote huggigface.co (missing letter). The test that tried to hit it failed cleanly with the block message. Before AgentGuard the same typo would have been a 404 from a parked domain, which is harder to spot.

What it is not

This is not a WAF. It is not a sandbox. A determined attacker who can run arbitrary code on the same process can disable the guard. The threat model I care about is: my own LLM agent, running my own tools, deciding on its own to call a host I did not approve. That covers most of the prompt-injection-via-content cases I worry about in practice.

It also does not do DNS. The check is on the hostname string the agent passes in. If you let the agent supply raw IPs, add IP rules too, or resolve first and check the result.

Three flavors

I shipped this in three runtimes because I keep switching between them:

  • npm: agentguard
  • PyPI: agentguard-py
  • crates.io: agentguard-rs (with a reqwest-middleware feature so the check fires automatically on every outbound request)

The Rust one is the version I reach for first now because the middleware bit means I cannot forget to wrap a tool.

Wrap

Cost was small. Maybe an afternoon to write, a few days to use across my agents. The payoff is one less category of incident I have to think about.

Repo: https://github.com/MukundaKatta/agentguard

If you run agents that have any kind of fetch tool, having a domain allowlist as the floor is cheap insurance.

Top comments (0)