The technology behind Webamend

What actually happens between a client typing a sentence and a pull request appearing in your repository, step by step, with nothing left out.

The idea of a coding agent editing a client's website sounds reckless the first time you hear it. It sounded reckless to me, and I built it. So this post is the long version: every stage a request passes through, what the agent can and cannot reach at each one, and where the numbers come from. If you only want the short version, it is on the home page. This one is for the person who wants to know before they hand over a repository.

Seven stages

A change moves through seven stages, and each one is a separate piece of code with a separate job. I will name them the way the code does.

Ask. The client types a request in the editor and picks an effort level, from free to extra. They can attach a file, usually an image. That is all they can do. The editor has no other controls.

Prepare. The app takes a lock on the site so two changes cannot run at once, reads the site's configuration and its policy file, and creates a temporary working tree: a fresh copy of the repository's files with no .git directory and no credentials of any kind.

Edit. A throwaway container starts, receives the working tree and the request, and runs the agent. When the agent says it is finished, the container is destroyed. More on what is inside it below.

Gate. Back on the host, the app compares the edited tree with the original and checks the difference against the policy file. A path that is not allowed, a change that is too large, a new dependency, a script loaded from somewhere off-site: any of these and the whole change is discarded and the operator gets an email saying which rule it broke.

Push. A change that passes is committed to a branch named after the conversation and pushed to GitHub, and a pull request is opened or updated. The client's request is written into the pull request, so the trail reads as a conversation.

Preview. Netlify sees the branch and builds a Deploy Preview, as it would for any branch. The app watches for the preview URL and streams it to the client's page, along with a short trail of what has happened so far.

Publish. The client presses Publish. The app merges the pull request. Netlify builds the live site from the merge. Undo reverts that merge commit and Netlify builds again.

Nothing in that list is novel. Branches, pull requests, preview builds, a merge: this is how a developer ships a change. The only new part is who types the request.

The sealed room

The container in the Edit stage is where most of the risk would be if there were any, so it is worth being precise about it.

The container has the working tree, the request, and the model. It does not have Git, so it cannot commit, push, read history or reach a remote. It does not have any credentials: no GitHub token, no Netlify token, no model API key, nothing in the environment. It does not have a network route to the repository, the live site, the host application, or the internet at large. When the agent finishes, the container is removed, and the next request gets a new one.

What comes out of the container is a set of files. That is the only output. The host reads the files, and the host decides what happens next. The agent never decides anything about publishing, because there is no code path from inside the container to a commit.

I set it up this way because I did not want to have to trust the agent. I trust the gate, which is a few hundred lines of code I can read, and the gate does not care how clever the model was.

The policy file

The rules live in a file called .webagent/policy.yml in the client's repository. Here is one with every key set:

allow:
  - 'src/components/**'
  - 'public/images/**'
deny:
  - 'src/lib/payments/**'
maxFilesChanged: 15
maxDiffLines: 800
forbidNewDependencies: true
forbidExternalCode: true

allow is the list of paths the agent may change. deny cuts holes in it. The two limits cap the size of a single change, in files and in added-plus-removed lines. forbidNewDependencies refuses a change that adds to a vendor directory or a lock file. forbidExternalCode refuses a change that adds a script or stylesheet loaded from another domain.

The defaults are permissive on paths and strict on everything else: allow everything, deny nothing, fifteen files, eight hundred lines, and both forbids on. On the first client site the policy allows the whole tree, because it is a brochure site with nothing to protect. On a site with a checkout, I would fence the payments code on the first day.

The client never sees this file. It is not in the editor and it is not mentioned anywhere they can reach. If you change it, the next request obeys the new rules.

Effort, not model names

The client picks an effort level, and the app maps effort to a model. As of this writing:

Effort Model, via OpenRouter
free a small free-tier coding model
low DeepSeek V4 Flash
medium Claude Sonnet 5
high Claude Opus 5
extra Claude Fable 5.1

The client does not need to know any of these names, and the mapping can change without them noticing. What they experience is that a wording tweak on free is fine and a new section on medium is better. Most requests on the one production site have been free or low, which is why the running cost per change is pennies.

The models are reached through OpenRouter, so the customer holds one key and gets one bill for tokens, with no vendor accounts to manage. Hosted or self-hosted, the key is theirs and the tokens are charged to it.

GitHub is the database

Webamend has no database. I want to say that plainly because people keep asking where the data is.

The conversation is the pull request. The history is the Git history. The current published state is the main branch. The audit trail is the list of merged pull requests, each with the request and the person who made it. Undo is a revert. If you delete the Webamend installation tomorrow, none of that goes anywhere, because it was never in Webamend.

This is also why setup is by hand and takes a day rather than being a signup form. Connecting a site means creating a Git deploy key, a Netlify token scoped to the site, the policy file and the list of allowed addresses, and checking that a preview actually builds. I would rather do that once and have it right.

Where it runs

The hosted version runs on a plain Linux server. Each client gets its own Linux user with a rootless Docker daemon in a directory only that user can read. Each site is one Compose project under that user. Caddy sits in front and terminates TLS. The agent containers are pulled from a local registry so a request does not depend on the internet being reachable.

Signing in

The editor has no passwords. A client is invited by email address. Signing in is a link sent to that address followed by a six-digit code from an authenticator app. Nobody who is not on the list can reach the editor, and nobody on the list can reach a different client's editor, because each installation is a separate process under a separate user.

What you can read

All of this is public. The repository is on GitHub under the AGPL, and the sections above map onto directories in it. If you want to check that the container really has no network, the Compose file is right there. I would rather you read it than take my word for it.

If you would rather have all of this run for you, the hosted plans are priced per website, with the model tokens on your own OpenRouter key. If something in here is unclear, ask. If something in here is wrong, please tell me, because the whole design rests on it being right.