The CMS you talk to. A client edits their live website by describing what they want, in plain English, and an AI agent makes the change. The interesting part is not that it works. It is that the agent is structurally incapable of breaking the site.
Built by Christian Mauerer, AI Solutions Architect, at Lovepixel Agency. Running in production for a real client.
One sentence, and the heading changes. The preview is the client's real site, rebuilt and reloaded as they watch.
You build someone a beautiful site. Six months later they email you to change a headline, because learning an admin panel for one sentence a quarter is not a trade they want to make. That email costs them a day of waiting and costs you an interruption, on a task neither of you values.
Each of the usual answers moves the problem rather than removing it. A CMS gives them an interface to learn, and enough freedom to undo a layout they did not design. A page builder keeps them editing, at the cost of a design that drifts from the one you agreed. Doing it yourself indefinitely is the most reliable option and makes you a permanent dependency.
Those are trade-offs, not flaws. For a site with a real content cadence, a CMS is the right answer and we build plenty of them. This is for the other case: a client who wants a handful of changes a year, on a design that should stay exactly as delivered.
For that case, Spoken CMS is a fourth option. They say "make the hero heading say Welcome home" and it happens, and there is no combination of words they can type that takes the site down.
The second answer is the product. Asked to rewrite the parallax engine in 3D, it declines like this:
Rewriting the parallax engine in 3D requires changing the site's animation code, which lives outside the page content and design settings I'm allowed to edit. A developer would need to build that engine before I could adjust anything about it.
Nothing was changed.
Specific about where the boundary is, honest about what would be required, and no jargon. Note the counter goes from 39 to 38: a refusal is a real decision by the agent, so it costs a change. Only a rate limit or an infrastructure fault is refunded, because those are not the client's fault.
Both screenshots are the running product, captured by driving the real UI. No mockups.
The agent runs with real file-write tools against a real checkout of a live client site. Four independent layers stand between it and damage. Any one of them failing leaves the other three.
1. A path allowlist, enforced before the tool runs.
Exactly two globs are writable: src/content/pages/**/*.json and
src/config/site.config.json. Everything else in the repo is readable for
context and rejected on write.
2. A PreToolUse hook, as a second gate.
The allowlist is checked again by a different mechanism, because a single
enforcement point is a single point of failure.
3. A full-repo SHA-256 scan, after the run. Every file is hashed before and after. Any change outside the safe surface, however it got there, is detected and the whole run is reverted. This catches what an allowlist cannot: a subprocess, a symlink, a path the check did not anticipate.
4. A build gate.
npm run check and npm run build must both pass. If either fails, the edit
is reverted in full. A schema violation, a broken reference or a type error
means the change never existed.
Then, and only then, the diff is shown and the change is offered for saving.
A pre-launch security review found something none of the four layers caught, and the reason is the useful part.
The content schema accepts prose as a string. Strings can contain HTML. The renderer claimed, in a comment, that it escaped HTML:
// with `html: false` semantics enforced here, no content field can inject markuphtml: false is a markdown-it option. This project uses marked, which
has no such option and dropped its sanitize flag in v5. The comment described
a control that had never existed. Every payload rendered live.
Nothing was bypassed. The agent wrote prose into its permitted surface, the schema accepted a valid string, and both gates passed because neither inspects the inside of a string. The sandbox worked exactly as designed and the escape route ran straight through it.
It is fixed in two places now, either sufficient alone: the schema refuses anything tag-shaped on the way in, and the renderer escapes it on the way out. Both are covered by tests that run in the same gate the agent must pass, so a payload now triggers an automatic revert.
The general lesson is in the repo because it was expensive: a comment is not a control. It read as reassuring for exactly as long as nobody executed it.
Browser ──HTTPS──► Cloudflare ──tunnel──► Orchestrator ──HTTP──► Container
(Access + (holds every (holds one
WAF + TLS) secret, owns token, runs
the only push) the agent)
│
▼
sparse git clone
scoped to one site
The orchestrator holds every credential, owns the session lifecycle and the
three caps, proxies the preview, and is the only code permitted to push. Every
response body and error string passes through a redactor on the way out, so a
careless err.message cannot leak a token.
The container is a per-session sandbox: 2 GiB, 1.5 CPU, capabilities dropped to the five a bind-mount writer needs, and exactly one secret. It never sees the git token, the passphrase or the signing key.
Push scoping is verified by asking git, not by trusting the caller. The
check runs over the base..HEAD range, so a commit touching another client's
directory is refused even when it is hidden behind a clean tip. That case is
covered by a test that constructs exactly that shape.
Three caps bound a session: 60 minutes wall clock, 20 minutes idle, 40 edits. They live in the orchestrator rather than the container, because a cap enforced by the thing being capped is a suggestion.
Portfolio repos usually show what went right. These are the failures that changed the design, because they are the part worth reading.
Session reuse is more expensive than starting fresh. The intuition is that keeping one agent session alive across edits saves money through prompt caching. A controlled A/B said otherwise: +127% cost. Cache reads grow linearly with turn count, so by edit five you are re-reading four edits of history to make one change. Every edit now gets a fresh session.
A 368-second edit proved the timeout rewrite necessary.
/message originally held the HTTP connection for the whole agent run. A real
edit measured 368 seconds. nginx defaults to a 60-second read timeout and
Cloudflare's proxied ceiling is 100 seconds, not configurable below Enterprise.
It is now asynchronous: 202 with a sequence number, and the UI polls. The
sequence number matters, because a reload mid-edit would otherwise render the
previous edit's outcome as the answer to this one.
A guard that refused only the configurations nobody runs. A dev-mode flag skipped authentication for loopback callers, fenced by two checks. The production shape is nginx in front of a loopback bind, which passed both and made every request on the internet look like loopback. Its own comment named the hazard precisely and the code failed to cover it. Now fenced by a positive assertion of dev instead: conditions that cannot both hold in production.
Mutation testing killed three tests written to catch specific bugs.
All three passed against the broken code they were meant to catch. One flooded
3,000 keys when the cap was 4,096, so eviction never ran. One hammered a frozen
clock, where re-arming a lockout computes the same instant. One asserted
x || true. Every fix in this repo is now verified by reverting it and
confirming a test fails — a test that cannot fail is worse than no test, because
it is counted.
One filed finding turned out not to be a bug. A review flagged that a correct passphrase during a lockout counted as a failure. True, and unobservable: the counter is zeroed when the lockout lapses. The line was tightened anyway, and the comment says plainly that no test claims to catch it. Reporting a non-bug as fixed is how a test suite becomes theatre.
orchestrator 96 passing sessions, caps, push scoping, auth, proxy
auth 43 passing token format, timing safety, cookie handling
content 36 passing the XSS boundary, both layers, in both directions
harness self-test path allowlist, revert, gate outcomes
The content tests are wired into npm run check, which the agent's own build
gate runs. A payload therefore triggers an automatic revert rather than a
review comment.
Requires Docker, Node 22+, and a Claude subscription token.
cp .env.example .env # fill in four values
./scripts/testdrive.sh # sandboxed: pushes to a throwaway local clonetestdrive.sh points the push target at a temporary directory, so nothing
reaches a real repository. Read docs/contracts.md before pointing it anywhere
that matters.
Running in production for one client. The repo is a case study rather than a product: paths and the site schema are specific to the deployment it was built for, and generalising it was not the goal.
The client site itself is not included. example/ contains the schema, the
renderer and the security tests, which is enough to see how the safe surface is
defined without republishing work that belongs to the client.
Featured with the client's permission.
MIT. See LICENSE.