Letting AI agents operate a server, inside the rules
I use Claude Code and Codex to do real work on my hosting platform: scaffold stacks, debug TLS, read logs, ship changes. That's only comfortable because the server tells them how things are done, and stops them when they shouldn't do something.
The problem with a helpful agent
An agent with a shell on a server can do in seconds what takes me minutes, and that includes the wrong things. "Free up some disk space" can mean docker system prune --volumes. "Check why the app can't connect" can mean printing a container's environment, secrets included, straight into a chat transcript. Agents are good at following instructions. They need the right instructions, and limits for when the instructions aren't enough.
Layer 1: one rulebook, loaded automatically
The platform's repository has an AGENTS.md: how the server works, naming, paths, which network a tenant uses, where secrets live, what needs a human, plus a table of open issues and decisions already taken. It's written for an agent, so it's concrete: commands, paths and examples, not principles.
- Codex reads
AGENTS.mdnatively. - Claude Code gets a
CLAUDE.mdthat simply imports it, so there's one source of truth. - A short global instruction file on the host tells every session, started in any folder, to read the rulebook before touching Docker, DNS or deployments.
Recording decisions matters as much as recording rules. When I make a call on a trade-off, it goes into the table as closed, with the date and the reason, and agents stop re-raising it in every session.
Layer 2: a guard that runs before every action
Claude Code supports PreToolUse hooks: a script that sees each command or file write before it runs and can allow it, block it, or require my confirmation. Mine has three outcomes:
| Decision | Examples |
|---|---|
| Block | pruning Docker volumes, down -v, recursive deletes on data directories, formatting or partitioning disks, disabling the firewall, mounting the Docker socket, reading secret files, unfiltered docker inspect, printing a container's environment, writing anything that looks like a token into the repo |
| Ask me | sudo, editing or restarting shared infrastructure, stopping a production stack, force-pushing, piping a download into a shell, publishing a port |
| Allow | everything else: reading logs, scaffolding projects, linting, running dev and staging |
Static permission rules in the agent's settings add a second, simpler layer: secret paths can't be read with the file tools at all.
Secret hygiene shaped a lot of the rules. The agent never needs a secret's value, only its name, so every path that could print one is closed: no cat of secret files, no docker compose config without -q (it renders env_file contents), no docker inspect without a format filter. When the agent needs to prove credentials work, it runs a check inside the container that prints only an HTTP status code.
Layer 3: a backstop that doesn't care who wrote the change
Hooks only protect sessions that load them. A Codex session on my laptop, or me at 2 a.m., wouldn't be covered. So the same rules also live in a linter that checks every stack (pinned images, resource limits, no published ports, private databases, valid TLS labels) and runs as a Git pre-commit hook and in GitHub Actions on every push. Whatever the source, a change that breaks the rules can't land.
Test the guard like any other code
A guard is just pattern matching, and pattern matching has bugs. The guard has a regression suite of 43 cases: commands that must be blocked, ones that must ask, and ones that must pass. It runs in CI and whenever the guard changes.
The first version blocked its own maintenance. The rule for the shell's . file source command was written as an unanchored \., which matched any dot followed later by a secret path. Even editing agent-guard.sh tripped it.
False positives are cheap compared with a missed block, but they train you to work around the guard, which defeats its purpose. Each one now becomes a new "must allow" test case before the pattern is fixed.
What it's like in practice
I can ask an agent to "host acme's shop with a frontend and an API". It reads the rulebook, scaffolds the stack in the right place with the right names and tenant network, writes the env files for dev, staging and prod, lists the secret variables I need to create (without asking me to paste them), lints, starts staging, tests it through Traefik, and commits. When it reaches for production or shared infrastructure, it stops and asks.
When something breaks, it diagnoses from evidence: it reads Traefik's logs, checks the served certificate with openssl, queries the log platform, and tests credentials with a single read-only API call instead of retrying blindly.
Limits worth knowing
- The guard reads text, not intent. It can't always tell a command from a commit message that mentions one, so it errs on the side of blocking.
- Pattern rules aren't a sandbox. The agent runs as a user in the
dockergroup, which is effectively root on that host. The guard reduces the chance of a mistake; least-privilege accounts and backups limit its impact. - Rules drift unless they're tested and kept in the same repository as the code they protect.
The goal isn't to distrust the agent. It's the same thing we do for people: give them clear documentation, automate the guardrails, and require a second pair of eyes where the stakes are high.