Why this is worth reading: You can already see what an AI patch changed with a diff. What a diff does not tell you is which changes can take a service down, which ones need a config lint before they are safe, and how to recover when the patch is applied at 02:00. This guide turns that missing knowledge into a sidecar ledger: a reproducible artifact that names every changed config file, estimates the blast radius, states the validation command you must run, and records the exact rollback command. It is deliberately boring, because review automation that depends on model output is useful only when it is deterministic.
The problem with reviewing AI-edited server configs as text
You have probably reviewed an AI-generated patch to an nginx virtual host, a systemd unit, or a crontab. Diff review encourages you to ask whether each line is syntactically plausible. But a config file is not a pure function: a one-line change can move a data directory, open a debug port, or change the user that a service runs as. The correct review question is not only 'does this look right?' but 'what does this file touch, which validator can prove it still works, and how do I put it back?' A text diff answers the first question only. The ledger answers the other two.
Workflow: snapshot, stage, verify, roll back
You keep two local trees: before/ and after/. Before you allow an AI-generated config change near a live host, you copy the current files into before/, apply the candidate into after/, and run the ledger. The after/ tree is not your production directory; it is a dry-run copy used only for comparison. Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you use the free model access in MonkeyCode to draft config patches, keep the candidate in an after/ directory on a free server and treat it as untrusted until the gate passes.
The script below compares the two snapshots and writes a Markdown table. Each row names a changed file, its state, the added and removed line counts, a suggested validator, and the rollback command.
#!/usr/bin/env python3
'''Create a review ledger for before/after server config trees.'''
from __future__ import annotations
import argparse
import difflib
from datetime import datetime, timezone
from pathlib import Path
def count_lines(path: Path) -> int:
try:
return len(path.read_text(errors='replace').splitlines())
except OSError:
return 0
def suggested_validator(rel: Path) -> str:
name = rel.name
if 'nginx' in str(rel).lower():
return 'nginx -t'
if str(rel).endswith(('.service', '.socket', '.timer')):
return f'systemd-analyze verify {rel}'
if 'cron' in str(rel).lower():
return f'crontab -T < {rel}'
if rel.suffix in {'.env', '.sh'}:
return f'sh -n {rel}'
return f'diff -u before/{rel} after/{rel}'
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('before')
parser.add_argument('after')
parser.add_argument('--out', default='change-ledger.md')
args = parser.parse_args()
before = Path(args.before)
after = Path(args.after)
before_files = {p.relative_to(before) for p in before.rglob('*') if p.is_file()}
after_files = {p.relative_to(after) for p in after.rglob('*') if p.is_file()}
rows = []
for rel in sorted(before_files | after_files):
bp = before / rel
ap = after / rel
if not bp.exists():
added, removed, state = count_lines(ap), 0, 'added'
elif not ap.exists():
added, removed, state = 0, count_lines(bp), 'deleted'
else:
seq = difflib.SequenceMatcher(
None,
bp.read_text(errors='replace').splitlines(),
ap.read_text(errors='replace').splitlines(),
)
added = removed = 0
for tag, i1, i2, j1, j2 in seq.get_opcodes():
if tag in ('insert', 'replace'):
added += j2 - j1
if tag in ('delete', 'replace'):
removed += i2 - i1
state = 'modified'
if state == 'deleted':
rollback = f'cp -p {bp} {ap}'
elif state == 'added':
rollback = f'rm -f {ap}'
else:
rollback = f'cp -p {bp} {ap}'
validator = 'verify no remaining references' if state == 'deleted' else suggested_validator(ap)
rows.append((rel, state, added, removed, validator, rollback))
now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
with open(args.out, 'w', encoding='utf-8') as f:
f.write('# Change ledger' + chr(10) + chr(10))
f.write(f'Generated: {now}' + chr(10) + chr(10))
f.write('| file | state | added | removed | validation | rollback |' + chr(10))
f.write('| --- | --- | ---: | ---: | --- | --- |' + chr(10))
for rel, state, added, removed, validation, rollback in rows:
f.write(f'| `{rel}` | {state} | {added} | {removed} | `{validation}` | `{rollback}` |' + chr(10))
print(f'Wrote {args.out} with {len(rows)} rows')
if __name__ == '__main__':
main()
A ledger row looks like this:
| file | state | added | removed | validation | rollback |
|---|---|---|---|---|---|
nginx/sites-available/default |
modified | 2 | 1 | nginx -t |
cp -p before/nginx/sites-available/default after/nginx/sites-available/default |
systemd/example.service |
modified | 1 | 0 | systemd-analyze verify after/systemd/example.service |
cp -p before/systemd/example.service after/systemd/example.service |
cron.d/backup |
added | 4 | 0 | crontab -T < after/cron.d/backup |
rm -f after/cron.d/backup |
Use the ledger as a merge gate, not a report
You run the validator for each modified file before you merge. For an nginx change, run nginx -t after the candidate is in place. For a systemd unit, run systemd-analyze verify after/example.service. For a crontab, run crontab -T < after/cron.d/backup. If a validator exits non-zero, you stop and roll back. You do not need to convince yourself the model is bad; the command already proves the config is invalid. This gate turns a subjective model-quality debate into a pass/fail procedure.
Why this is stronger than a diff-only review
You can see listen 80; became listen 8080;, but without the rollback column, your recovery path is another generated suggestion. With the ledger, the rollback is a known-good copy. You also capture the file class: an addition of a cron entry requires a different proof than an edit to an env file. The ledger stores that proof next to the change instead of leaving it in the reviewer's head.
Limitations and who should not use this
You should not use this ledger as a general static analyzer. It does not parse semantics inside application-specific files such as php.ini or my.cnf beyond the validators you assign. It compares only local snapshots, so changes inside environment variables, mounted secrets, or remote Kubernetes resources will not appear unless you export them into the trees. The script treats every file as text, so binary assets will produce misleading line counts. If a path contains spaces, the generated rollback commands need shell quoting. Most importantly, this workflow only helps when you stage first; if you copy the AI output directly into /etc/, you have already destroyed the before state and the audit trail is gone. This workflow is not for teams that already have full configuration management with server-side validation and versioned rollback; they should stay with their existing pipeline. It is for solo operators and small servers who are currently reviewing generated patches in a text editor.
Keep the ledger beside the diff, and you will spend less time arguing about model output and more time running the one command that actually proves the service still starts.
Top comments (0)