AI coding agents are good at infrastructure work that has a well-known shape: a Dockerfile for a Node or Python app, a systemd unit, an Nginx server block in front of an app, a CI pipeline that installs, tests and builds, a database migration, the tests around all of it. They produce that in seconds and it is usually close to right. They are bad at the details that change between versions - flags, config keys, defaults - and they say the wrong thing with exactly the same confidence as the right thing. And given a shell, they will run destructive commands as calmly as they run ls. The rule we work by is short: the agent writes, a tool validates, a human reads every command, and the agent never holds production credentials. Everything below is how that rule works in practice.
AI coding agents are now a normal part of how software gets built, and running servers on a hosting platform like RE:NODE gives us opinions about what a server should and should not have done to it. This post is those opinions. There are no war stories in it, only the principles we ended up with.
Where agents are fast and right#
The work agents do well is the work that has been written ten thousand times in public, with small variations. That covers a surprising amount of DevOps.
- Dockerfiles for common stacks. A multi-stage build for a Node, Go, Python or .NET app, with a slim runtime image, a non-root user and dependency install cached before the source copy. Agents know this pattern well and get the layer ordering right more often than a tired human does.
- systemd units.
ExecStart,WorkingDirectory,User,Restart=on-failure,EnvironmentFile, the[Install]section. Ask for hardening and you get a sensible set ofProtectSystem=,NoNewPrivileges=and friends. - Nginx reverse proxy blocks.
proxy_pass, the forwarded headers, WebSocket upgrade headers, a redirect from HTTP to HTTPS. The common case is solid. - CI pipelines. A GitHub Actions workflow that checks out, sets up a runtime, caches dependencies, runs lint and tests and builds an artefact. Agents write this faster than anyone can find the last repo where they did it.
- Migrations and tests. Given a schema and a change, they write the migration and, more usefully, the tests that pin the behaviour. Tests are where the most value per minute comes from, because a generated test that fails is information either way.
- Glue scripts. Log rotation helpers, backup wrappers, a script that checks certificate expiry. Short Bash and Python with clear inputs and outputs.
What these have in common: the shape is stable across years, mistakes show up immediately when you run or validate the thing, and the cost of a wrong first draft is a minute of review. That is the zone where an agent is a plain win, and we would not go back to writing these by hand.
There is a second, quieter win: reading. An agent that is allowed to read a repository, a config directory and a few log files, and then explain what is going on, is often faster than grepping around a system last touched months ago. Reading cannot break anything. More on that below.
Where agents are confidently wrong#
The failure mode is not that agents produce nonsense. It is that they produce plausible config that was right at some point, for some version, on some distribution - and nothing in the output tells you which part is out of date.
The patterns to watch for:
- Invented flags. A CLI flag that sounds like it should exist, in the naming style of the tool, that does not. It looks exactly like the real flags around it.
- Outdated keys. Config that was correct a few major versions ago. The model has seen far more old documentation than new, so old forms are over-represented.
- Mixed versions. A file where half the directives come from one major version and half from another. Each line looks fine; the combination never existed.
- Distribution blur. Paths and package names from Debian in a file meant for a RHEL-family box, or the other way round.
- Silent defaults. An option set explicitly to what the agent believes the default is, where the default changed, or an important option omitted because the agent assumed a default that is not there.
Some real examples of drift that agents reproduce, all of which you can check against the upstream docs:
| What the agent writes | Current form | Why it matters |
|---|---|---|
listen 443 ssl http2; | listen 443 ssl; plus http2 on; | Nginx 1.25.1 deprecated the parameter |
ssl on; | ssl on the listen line | Deprecated since Nginx 1.15.0, later removed |
version: "3.8" in Compose | omit it | Compose ignores it and warns |
MemoryLimit= in a unit | MemoryMax= | The old name is cgroup v1 era |
apt-key add | a keyring in signed-by= | apt-key is deprecated |
echo "::set-output ..." | write to $GITHUB_OUTPUT | GitHub deprecated the command |
None of these is exotic. That is the point. The wrong form is the one most of the internet used for years, so it is the one the model reaches for first. Some of these still work with a warning, which is worse than failing: the file goes into the repo, the warning goes into a log nobody reads, and the next upgrade removes the old form.
An agent's confidence tells you nothing about its correctness. A wrong config key and a right one arrive in the same calm tone.
The uncomfortable part is that the agent will also defend the wrong answer if you ask "are you sure?". Sometimes it corrects itself, sometimes it doubles down, and sometimes it apologises and changes a correct line into a wrong one. Asking the model is not verification. Running the tool's own checker is.
Destructive commands and why the shell is the dangerous part#
Writing a file is cheap to undo. Running a command often is not. The jump from "agent edits files in a repo" to "agent runs commands on a machine" is where the real risk sits, and it is the step people take most casually.
Commands we treat as never-without-a-human, whoever proposes them:
rm -rfwith a variable in the path. If the variable is empty,rm -rf "$DIR/"becomesrm -rf /. Agents write this pattern because humans do.docker system prune -a --volumesordocker volume rm. Volumes are where databases live. "Clean up disk space" is a perfectly reasonable request that ends with an empty database.docker compose down -v. The-vremoves named volumes. It looks like a tidy flag.git reset --hard,git clean -fdx,git push --force. Fine on a scratch branch, ruinous on a shared one or a server with uncommitted local config.DROP,TRUNCATE,DELETEwithout aWHERE, or a migration that drops a column in the same release that stops writing to it.chown -Randchmod -Rfrom the wrong directory.- Firewall changes over SSH.
ufw enablebeforeufw allow OpenSSH, oriptables -Fon a chain whose default policy isDROP, and the session you are typing in is gone. terraform applyoransible-playbookwithout first looking at a plan or a check run.
Say an agent is asked to fix "disk full" on a box at 3am. The quickest path to free space is deleting the biggest thing, and the biggest thing on a server is very often the data you are paid to keep. A person would hesitate there. An agent optimising for "task completed" does not, unless you have arranged things so it cannot.
The firewall case deserves its own note because it does not destroy data, it destroys access. If you are letting an agent near a firewall at all, the safe order is to allow SSH first and keep a second session open while you enable it - the UFW guide covers that sequence.
Guardrails: read-only first#
The single most useful guardrail costs nothing: start every infrastructure session with the agent reading, not doing.
Reading means looking at config files, listing services, reading logs, running status commands. On a server that means things like systemctl status, journalctl -u app --since "1 hour ago", ss -tlnp, df -h, docker ps, nginx -T. None of these changes state. Let the agent build a picture and propose a change as a diff or a list of commands, then decide.
Claude Code has a plan mode where it reads and proposes without editing, and both Claude Code and Cursor can be set to ask before running terminal commands. Leave that on for anything that touches infrastructure. In Claude Code you can go further with permission rules in settings.json, allowing the read-only commands so they do not prompt and denying the ones that should never run unattended:
{ "permissions": { "allow": [ "Bash(systemctl status:*)", "Bash(journalctl:*)", "Bash(docker ps:*)", "Bash(nginx -t)", "Bash(git diff:*)" ], "deny": [ "Bash(rm -rf:*)", "Bash(docker system prune:*)", "Bash(git push --force:*)", "Read(./.env)", "Read(./secrets/**)" ] }}A deny list is a seatbelt, not a wall. Commands can be phrased many ways, and a determined chain of shell can get round a prefix match. Treat these rules as a way to stop the common accident, and keep the real protection where it belongs: in what the agent's environment can reach at all.
Guardrails: dry runs, review and staging copies#
After read-only, the next layer is making every change show you what it would do before it does it. Most serious infrastructure tools have a mode for this, and an agent should be told to use it every time.
$ terraform plan -out=change.plan$ ansible-playbook site.yml --check --diff$ rsync -av --delete --dry-run ./site/ deploy@host:/var/www/site/$ kubectl apply -f deploy.yaml --dry-run=server$ kubectl diff -f deploy.yamlThen read every command before it runs. Every one. That sounds slow and is not, because most commands in a session are reads, and reading a one-line command takes seconds. The ones that change state are the ones worth the seconds. The moment you notice yourself approving without reading, switch the agent back to asking for a plan instead.
Review the diff, not the summary. Agents write good summaries of what they changed, and the summary is sometimes not what the diff says. "Updated the Nginx config to add caching" can come with a removed location block that was not mentioned. git diff is the source of truth.
The last layer is where the agent works. Agents should work against a local checkout, a container, or a staging copy - never against production directly. A staging copy is a second environment with the same shape as production, its own credentials and its own data, where a wrong command costs a rebuild rather than an apology. We have written about the practical side in staging and production on one account and, for game servers, running a test server beside production. The agent can be as fast as it likes there. Promotion to production is a separate, boring, human-reviewed step: a merge and a deploy pipeline, not an agent with SSH.
Never give an agent production credentials#
This is the rule to keep if you can keep only one.
An agent session reads a lot of text you did not write: dependency source, README files, issue comments, web pages, log lines, error messages from third-party services. Any of that text can contain instructions, and current models can be steered by instructions hidden in content they read. That is prompt injection, and nobody has a complete fix for it. Combine it with credentials and a shell, and the question is not whether the agent is well-intentioned but whether every piece of text it reads is.
So the practical rules:
- No production database URLs, cloud keys, SSH keys to production hosts or deploy tokens in the agent's environment. Not in
.envfiles it can read, not in shell history, not pasted into the chat "just this once". - The agent works with staging credentials that are scoped to staging and could be leaked without much harm.
- Deploys to production happen through CI with its own secrets, triggered by a merge a human approved. The agent can write the pipeline. It does not run it.
- If an agent needs to look at production, it reads exported logs or metrics, or a human runs the read-only command and pastes the output.
Where you run servers through a panel rather than raw SSH, least privilege applies the same way: give each person and each tool only the access it needs, for as long as it needs it. The idea is laid out in subusers and least privilege, and the handling of secrets themselves in environment variables and secrets.
How to verify generated config#
Every config format worth using has a checker, and the habit that matters is running it before anything reloads. Ask the agent to run the validator as part of the change, and read its output yourself.
| File | Validate with | Catches |
|---|---|---|
| Nginx config | nginx -t | Syntax, unknown directives |
| systemd unit | systemd-analyze verify app.service | Unknown keys, bad paths |
| Dockerfile | hadolint Dockerfile | Bad practice, pinning |
| Compose file | docker compose config | Schema, interpolation |
| GitHub Actions | actionlint | Expressions, deprecated syntax |
| Shell script | shellcheck script.sh | Quoting, empty variables |
| sshd config | sshd -t | Syntax before you lock yourself out |
| sudoers | visudo -c | A broken file that kills sudo |
Validation catches syntax and unknown keys. It does not catch config that is valid and wrong - a proxy_pass to the wrong port, a unit that starts as root because User= was left out, a cache that stores responses with cookies. For that, check three things by hand:
- Every flag and key you do not recognise goes against the upstream documentation for the version actually installed.
nginx -v,docker --version,systemctl --versionfirst, then the docs for that version. This is where invented flags die. - What it does when it fails. What happens when the app crashes, the upstream is down, the disk fills. A unit with
Restart=alwaysand no limits restarts a broken app forever; that may or may not be what you want. - Who it runs as and what it can touch. User, file permissions, exposed ports, mounted volumes. Agents default to working rather than to locked down, so the security posture is yours to set.
Then the change goes to staging and gets exercised: the service restarts cleanly, the health check passes, the logs are quiet. Only then does the same file go to production through the normal path. For the specific formats, the systemd guide, the Nginx reverse proxy guide and Docker Compose for small stacks are what to check generated output against.
Migrations, CI and tests deserve extra suspicion#
These three look like ordinary code, so they get ordinary review. They should get more.
Migrations are code that runs once against your only copy of the data. An agent will happily write a migration that renames a column, which on a live system is a drop and an add as far as the running application is concerned. It will write a backfill that locks a large table. It will write a down migration that cannot actually restore what up removed. Our rule: every generated migration runs against a restored copy of real-sized data before it goes anywhere near production, and destructive changes are split across releases - stop using the column, deploy, then remove it. Migrations without downtime covers the expand-and-contract pattern, and it is worth making the agent follow it explicitly, because it will not by default. And the restored copy only exists if your backups restore, which is its own discipline: backups that actually restore.
CI pipelines run with secrets. A generated workflow that uses pull_request_target, checks out the pull request's code and runs it has handed your repository secrets to anyone who opens a pull request. A workflow that pins third-party actions to a moving tag rather than a commit trusts whoever controls that tag. Agents produce both patterns because both are common in public repositories. Read the triggers and the permissions block of every generated workflow, and set permissions: explicitly to the minimum.
Tests are the subtle one. An agent asked to make the tests pass has two ways to do it: fix the code, or change the test. It sometimes picks the second, quietly - loosening an assertion, adding a skip, mocking the very thing under test. A green test suite from an agent is not evidence until you have looked at what changed in the tests. When reviewing an agent's diff, read test changes first.
How an AI-assisted loop works#
When agents are the main build loop rather than an assistant, the human job changes. People write less and review more, and the review is the actual work. The principles to hold the loop to:
- Small, reviewable changes. One concern per change. An agent will happily refactor a deploy script, a Dockerfile and the CI pipeline in one go; three diffs that can each be read in a few minutes are better.
- The agent's claims are not evidence. "Tested and working" means nothing until you see the command and its output. "This flag exists" means nothing until you see the docs.
- Validators and tests in the loop, not after it. The agent runs
nginx -t,shellcheck, the test suite, and reads the failures itself. That catches most of the drift before a human ever sees the diff. - Infrastructure changes go through the same pipeline as code. No agent-run hotfixes on servers. If it is worth changing, it is worth a commit.
- Humans own merges and production. The agent can propose anything. It cannot ship anything.
- Write down the project's rules for the agent. A short instructions file in the repo - which commands are safe, which environments exist, what never to touch - does more than reminding the agent in every session.
The payoff is real. Boilerplate infrastructure that used to eat an afternoon is a few minutes of review, and that time goes into the parts that need judgement: what the failure modes are, where the data lives, who has access. The agent makes the easy part faster. It does not make the hard part go away, and pretending otherwise is how a fast loop becomes a fast way to break things.
What we would not hand to an agent#
Some work stays with humans, not because agents cannot produce something, but because the cost of a plausible wrong answer is too high and too hard to spot:
- Incident response on a live system. An agent under "fix it now" pressure is exactly the setting for a destructive shortcut. It can read logs and suggest causes; a human types the commands.
- Anything irreversible on real data. Restores over a live database, deletions, retention changes. These are rare enough that the speed gain is small and the downside is unbounded.
- Access control design. Who can reach what, firewall policy, IAM roles. Agents write the syntax well and the policy poorly, usually erring towards whatever makes the error go away.
- Version upgrades of core infrastructure. Database major versions, kernel and init changes. The knowledge an agent has is weakest exactly where the release notes are newest.
Everything else is fair game, provided the guardrails above are in place.
FAQ#
Is Claude Code or Cursor better for DevOps work?
Both are capable at the same core tasks, and the model behind them matters more than the editor. What matters for infrastructure is the control you have over command execution: approval before running commands, allow and deny rules, a read-only planning mode. Pick the one whose controls you will actually keep switched on.
Can I let an agent SSH into my server?
Into a disposable staging machine, yes, with commands approved one at a time. Into production, no. A production change should be a reviewed commit deployed by your pipeline, and if an agent needs production information, a human can run the read-only command and share the output.
How do I stop an agent inventing config options?
You cannot stop it, only catch it. Tell the agent the exact versions installed, have it run the tool's own validator after every change, and check any key or flag you do not recognise against the documentation for that version. Pasting the relevant section of the current docs into the session also helps a lot.
Are AI-generated Dockerfiles safe to use?
The common patterns usually are, once reviewed. Check that the base image is pinned to a real, current tag, that the final stage runs as a non-root user, that no secrets are copied in or passed as build arguments, and run hadolint on it. Then build it and run it in staging like any other image.
Should the agent write the tests as well as the code?
Yes, it is one of the most useful things it does. But read test changes before code changes in every diff, because an agent trying to get to green may weaken an assertion or skip a test rather than fix the bug.


