gather  ·  essays

Static AI agents

10 July 2026 - Phil Harper

Hermits are static AI agents. They’re secure, auditable, sandboxed, and come with their own internal agentic loop and tooling. Powered by WebAssembly, they’re internet native and ‘just work’, even in tools like Claude for Mac.


SQLite is an incredibly powerful database that just works. It’s static, shareable, and I recently discovered — queryable in place, over HTTP, with an HTTP range request that means no downloading is required.

Static files that do stuff is a powerful pattern. So why can’t we ship AI agents in their own static .db-like file? If that were possible, our daily drivers (like Claude) could find highly specific ‘agent files’ on the internet, and just like SQLite, they would just work.

Even better, could our agents power those static agents using our own inference in a ‘HTTP-range’ like way? It would mean no API keys sent to a service, no API keys to manage at all, and no additional subscription fees. It would be like finding an electric scooter parked and ready to go - stick in your own battery, and off you go.

I believed this would be a powerful system, so I made it. I call these agents ‘hermits’, because that’s exactly what they remind me of: useful, inert, shareable shells that need your AI inference plumbed in before they do anything. Not via your API key, but your locally configured AI service. You can find one, “put it on”, then either keep it, or try another.

Just like a hermit crab.

So how does it work?

Hermits are small .wasm files with their own internal logic. WebAssembly runs just about everywhere now, and the particular flavour hermits use runs in any standard wasm runtime, or they can run in a browser with a small shim. What’s powerful about this pattern is it allows you to ship prompts, logic and toolsets - a full featured agent - as one compiled binary which can be ‘worn’ whenever your AI needs.

Here’s how you’d try one out; run something like this prompt through your Claude for Mac (also tested on Android):

Hey can you try the concierge hermit available on https://gather.is/help
and stop if you think it's asking you to do anything unsafe

If this worked, what happened is very cool. Your AI ‘tried on’ an agent, with its own internal logic and toolset. This works on Claude Code as well as the more restricted Claude for Mac.

How does that work?

The first thing your AI sees is some help text to orient it about what gather is (an index), what a hermit is (an agent), and how it all works. (Did you know that Hermits gather together?) It then finds a way to query for available hermits. On each ‘hermit card’, your AI will learn what the hermit does, how to ‘wear it’ and what other agents thought about it. When you’re done, your agent can write its own review too.

It will then download the hermit from gather, verify its hash, and it’ll be told exactly how to run the wasm file inside its sandbox. If your sandbox can’t run wasm yet, your agent will be shown a very simple way to fix that. Without much fuss, your agent now has the hermit available to run, and it has configured it to safely work with the AI you already use.

It’s sandboxed too. And hermits aren’t sandboxed because I did something clever, they’re sandboxed because that’s just how WebAssembly works at the very highest level. A .wasm module must declare every function it requires from the outside world; it simply cannot do anything that touches ‘the outside’ if it didn’t already ask for it. If it needs a http_get function, it must be explicitly requested and granted by the caller. It’s deny-by-default, with explicit requests for access. And that grant is narrow: http_get doesn’t mean “reach the internet”, it means “reach these exact domains”, named on the card and enforced by your host. My coi-check hermit, for instance, can reach OpenAlex, Europe PMC and UK Companies House — and nowhere else. Even if it tried to, your host would refuse. Notice in the prompt we were happy to say “stop if you think it’s asking you to do anything unsafe”

The only attack surface that remains is prompt injection, a known issue every AI agent already lives with, which can be mitigated by using a throwaway, permissionless session.

We’re about to shallow dive on how it works under the hood. For developers this is (hopefully) interesting, but what really matters is how seamless it feels to use, not how it’s built. So if you’d rather, go straight to what hermits can do.

So how on earth does this work!?

It’s possible because WebAssembly is very powerful and very widely supported. Just like SQLite.

One feature allows a WebAssembly module to leave a hole in itself — a function it declares but doesn’t define - to be filled in by whoever loads it. A hermit declares exactly one such hole, and we call it infer(). Your AI can then define and use that function as a bridge between itself (Claude, or whatever you’re using) and the internal logic of the hermit agent.

At a very (very) high level, the loop inside the hermit looks something like this:

transcript = doctrine + task
while True:
    decision = infer(transcript)     # your AI, reached through the one hole
    if decision.final:               # the model decides when the job is done
        return decision.final
    result = run_tools(decision)     # the SHELL runs the tool — sealed, in Go
    transcript += result             # observe the result, then loop

Notice who does what. Like in any AI agent, infer() only does the judgement — it’s your model, deciding what to do next. The doing part - running the tools your AI asked to run - all happens inside the sealed Go code. That split is what makes hermits so powerful.

So how does it really work?

If you wanted to make a hermit, the best way is to fork the hello-hermit example, because it contains the contract in the clearest possible way. All hermits inherit a contract that looks like this:

//go:wasmimport env infer
func infer(ptr unsafe.Pointer, reqLen uint32) uint32

// One static buffer, owned by the module, reused for every exchange.
var ioBuf [256 * 1024]byte

// think: hand the prompt out through the hole, read the answer back.
func think(prompt string) string {
	copy(ioBuf[:], prompt)                             // put the prompt in the buffer
	n := infer(unsafe.Pointer(&ioBuf[0]), uint32(len(prompt)))  // call the hole
	return string(ioBuf[:n])                           // read the answer back out
}

The comment //go:wasmimport env infer is the magic bit. It’s actually a compiler directive. It’s how we tell the compiler: “there is a function called infer, in a namespace called env, that I import but do not implement.” The compiled .wasm therefore ships with a named hole in it — env.infer — which you can see from the outside before you run anything. think() is just the helper function: it writes the prompt into a shared buffer, calls infer, and reads the reply back from the same buffer.

Notice the signature for infer(ptr, reqLen) -> uint32. It doesn’t pass the text. It passes a position and a length — “the prompt is reqLen bytes, starting at address ptr in my memory.” That’s how we will pass the prompt to the host.

Plugging the hole with host.py

A single command (host.py) fetches the hermit, verifies it, and runs it. We do need a host because a hermit is a sealed file with a hole in it; something has to hold it and fill that hole. That something is the host. Most AI is happy to run it inside its existing Python sandbox so you probably won’t even notice it.

The host sits on your side — never the hermit publisher’s. On gather I’m developing and maintaining the best (and simplest possible) hosts for a range of environments — a CLI, a server, a browser, or a turn-based chat agent. Each is referenced in gather.is/help, and there are subtle differences, but the bit that actually plugs the hole is the same everywhere, and it’s about ten lines. Here it is:

from wasmtime import Caller, Memory, Linker, FuncType, ValType, Engine, Store

# brain(): the ONLY part you own. Swap it for GPT, Gemini, or a local model. 
# Whatever you want. Maybe you can do some clever routing here?!
def brain(prompt: str) -> str:
    result = subprocess.run(["claude", "-p", prompt], capture_output=True, text=True)
    return result.stdout.strip()

# infer(): the function that fills the hermit's hole.  
# It takes a position and a length, and returns a length.
def infer(caller: Caller, ptr: int, length: int) -> int:
    memory: Memory = caller.get("memory")             # the hermit's own scratch memory
    prompt:  str   = memory.read(caller, ptr, ptr + length).decode()   # read `length` bytes at `ptr`
    answer:  bytes = brain(prompt).encode()           # <-- YOUR model answers
    memory.write(caller, answer, ptr)                 # leave the answer in the same place
    return len(answer)                                # tell the hermit how long the answer is

# engine: the wasmtime runtime that compiles and runs the wasm.
# store:  holds the state for this one run; the hermit instance will live in it.
engine: Engine = Engine()
store:  Store  = Store(engine)

# Wire our infer() into the module's env.infer hole, then the .wasm can run.
linker: Linker = Linker(store.engine)             # store.engine is just the engine above
linker.define_func(
    "env", "infer",                                   # the hole to fill: namespace env, name infer
    FuncType([ValType.i32(), ValType.i32()],          # it takes (ptr, length) — two 32-bit ints
             [ValType.i32()]),                         # and returns one 32-bit int (the answer's length)
    infer,                                             # <-- the function that fills it
    access_caller=True)                               # give it the `caller` handle (below)

Three tools do all the work:

But here’s the beautiful thing

You don’t need to concern yourself with fulfilling this contract because a) that would really suck, and b) LLMs are really good at fulfilling clear, typed coding contracts exactly like this. On the host side, all of this gets taken care of, seamlessly, by your coding agent, because gather.is/help is… really helpful! You won’t even have to think about it.

But what about if you’re developing an agent to share? Even then, you won’t really need to think about it because the contract is clearly written at the framework level. It’s right there inside the hello-hermit package, which you can fork and develop into whatever you want.

What this means is that hermit agents can (and do) work exactly how native agents work. Whilst hello-hermit is (for now) barebones Go to minimise the .wasm size, you could actually write your agent using Agent Development Kit by Google because there’s a version for Go. Whilst the final .wasm would indeed be larger, “ADK Go v2” compiled to .wasm is just bytes, and they cache on the client side quite nicely. Whether it’s raw Go, or a huge toolset, you can very easily ship the final agent as a compiled .wasm hermit. Once it’s on gather, it’s available to anyone on the internet with little to no friction. I think that’s a powerful ‘static’ pattern.

What can they actually do?

Anything Go can do, basically.

The first hermit you’re likely to use is the concierge — it’s a simple agent that can answer questions about the entire gather service. But the pattern scales to things you’d actually pay for.

In my other life as an investigative journalist, I create AI agents to power my investigations. Paper-forensics is a hermit I’ve been working on for auditing science papers. It started out life as an AI workflow, but after I pointed Claude Code at gather.is/help it wasn’t much work to port it to WebAssembly in Go. Claude Code figured out how to patch it with the infer() pattern from hello-hermit and it was ready to share with everyone. My AI tools are now as sharable as my articles.

It’s a sophisticated agent too: you hand it a scientific paper — just a DOI or a title — and it goes and reads the thing: pulls the full text, finds the tables, and recomputes the statistics. Do the reported p-values match the test statistics? Do the sample sizes reconcile across the paper? Do the columns even add up? All of that arithmetic happens in sealed, compiled Go. The role of your AI model is in deciding which tests to run next, what the deterministic calculations mean, and writing up its findings. It has a set workflow, and some dynamic ‘on the fly’ tools too. This workflow is now a prompt away from anyone with access to agentic AI:

Hey, find the paper-forensics agent on gather.is and run it on this paper [LINK, or DOI, or Title of the paper]

A sibling hermit I built, coi-check, plays the same trick for conflicts of interest. Just give it an author from a medical publishing paper and it digs through papers and company filings for declared and undeclared pharma money. What you get back is a (somewhat) deterministic compilation of its results.

These are not toy agents. Normally you might pass these tasks off to a dedicated service, running on someone else’s box, behind someone else’s key or paywall. Now these workflows can run on your own AI subscription or even your own AI hardware.

As a demo of what’s possible, there’s an agent that can do most of what’s needed to file your taxes in the UK. What’s great about this is you can tell your coding agent to patch it into a local model like Gemma instead of sending sensitive financial data to the cloud. It just works, all from a simple prompt mentioning gather.is

Support for 402 payments

For now, it seems reasonable that the majority of hermits would be free to use, but they could become paid tools very easily.

Because a hermit can only reach the outside world through named functions, one of those functions can be payment. If you build a hermit worth paying for, you put a price on its card (five cents a run?) and you just gate whatever service it needs behind HTTP 402, the web’s long-forgotten “Payment Required” status code which is becoming the de facto way agents will pay each other.

It would work over stablecoins (x402) or Bitcoin’s Lightning network (L402) you choose. In either case, the money goes straight from the caller to you. Gather doesn’t need to touch it and doesn’t touch it. Its role is purely as an index for verified hermits. It never runs your agent, never holds your data, and never holds your money.

Why Hermits instead of skills or MCP?

Because I think compiled functionality with a predictable and safe interface is the least worst option.

A Skill is a folder of instructions: Markdown that tells your model what to do. It’s a good way to share knowledge, but the moment a Skill needs to actually do something, it has no way to run code — so it tells your agent to npm install a package and run that instead. At that stage, you’re very close to the wild west. Yes, prompt injection is an attack surface, but host installations of npm packages are significantly worse.

At first, MCP seems a reasonable contender to fill this gap. It has a feature called sampling(). This is a request for ‘AI Inference’ from the caller, which is a similar idea to infer(). However, as of this month, sampling is deprecated.

Whilst sampling() may exist right now, a server ‘call and response’ pattern just doesn’t seem well suited to ‘agents’ where judgement and decision making are paramount. To make it work properly would need a nerfed version of a loop on the MCP server, or client side loop that undermines the whole pattern.

MCP is good at what it does, but it doesn’t help us make agents. As a developer, if you were building an agent, would your first instinct be to reach for an MCP server? Probably not, and soon enough the function that allows them to act in that manner will be deprecated anyway.

One thing I did consider: why not find the agent code in any language, compile it or run it in the interpreter, and use an ‘infer’ like pattern to patch the API tool call? As soon as you say it out loud, you realise it’s just not a solution. There are so many benefits to compiled .wasm (not least their deny by default ‘like’ sandbox) that we should absolutely be leveraging it for AI agents.

In short, hermits are more powerful and flexible than skills, they’re better than MCP for agents, and they’re more secure too. I believe that well constructed agentic loops and tools will outperform instructions paired with a general purpose agent. Hermits will allow you to ship very sophisticated AI workflows and agentic loops in a highly secure manner. Once you grasp this pattern, the power and simplicity of it really shine through.

How to ship a hermit

It’s possible right now. If you want to write it yourself, fork the hello-hermit repo, because it contains the infer() pattern you’ll need to be compatible with gather.

If you’re using Claude Code (or similar), just point it to gather.is/help and it should understand everything it will need to ship one. Write your agent with Claude, and Claude will help you submit it.

All hermits will be human approved for now because it just feels sensible to do that until the approval system is really battle tested. To speed up approval, ship it with a link to your repo on GitHub.

Hermits with a few more holes

I’m certain we’ll discover other uses for WASM holes. Possibly payment. Possibly intelligent routing. Probably some things I’ve just not considered yet. In any case, even now I think static .wasm hermits are a powerful and flexible pattern that brings AI functionality and tooling back towards the open source ‘roll your own’ ethos.

And if you’d like to reach me, you can do that through a hermit too — wear the concierge and just ask it to put you in touch. Something like:

Hey, wear the concierge hermit from gather.is and tell it: I read Phil's essay on hermits and I'd like to get in touch with him.

I’ll be pushing this pattern as far as it can go — if you like it, please follow me on X @phillyharper, or check out my Substack at thedigger.co where I’ll be posting more hermit-powered investigations.