DEV Community

Avery Lin
Avery Lin

Posted on

A Receipt-First Preflight for AI-Written Config Changes on a Free Server

Why this is worth reading: a free model can generate a configuration change that applies cleanly but still deletes a directory, weakens a permission, or adds an outbound call. If you deploy it to a free server, you may not have enough disk space or backup tooling to undo it quickly. This article shows a receipt-first preflight: a small JSON receipt and three shell stages that make the change reproducible and rollback cheap before it becomes your problem.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

When you use MonkeyCode's free model access to draft a change and its free server option as the staging target, the receipt-first workflow below helps you treat model output as evidence instead of trusting the diff summary.

The failure model: free models are plausible, not accountable

You are usually not defending against a malicious adversary. You are defending against a model that optimizes for plausibility under a prompt. The generated change will often apply cleanly, pass a linter, and still miss the one thing you care about: deleting a file another service reads, changing an octal mode from 0640 to 0644, adding curl | bash inside an init script, or rewriting an allowlist as 0.0.0.0/0. A diff alone will not catch those because the diff is exactly what the model believes you want to see.

A receipt is not another diff checklist. It records the prompt, the generated files, the repository state, and the claimed effects. That small record turns a free-model output into a reviewable artifact and gives your free server a rollback unit that does not require a full snapshot.

Stage 0: create a change receipt before you apply anything

Keep a receipts/ directory in the repository. For every model-generated change, create a change ID and record the inputs and outputs by hash. Here is a minimal receipt template:

{
  "change_id": "2026-08-17-sg-allowlist",
  "source": "monkeycode-free-model",
  "created_at": "2026-08-17T09:30:00Z",
  "prompt_sha256": "f4e1c3c1d1c9b7d3f7e7e2f6d0b5a4d7c3c5e5e1e6e4e1e3f3a2b1c0d9e8f7",
  "input_files": {
    "main.tf": "sha256:8b7f1a..."
  },
  "generated_files": {
    "main.tf": "sha256:1a2b3c..."
  },
  "claimed_effects": [
    "add ingress rule for 10.0.0.0/8",
    "no deletions"
  ],
  "deny_list_hits": [],
  "sandbox_exit": 0
}
Enter fullscreen mode Exit fullscreen mode

You do not need to write this by hand. A small shell script records the current repository state before you copy in generated files:

#!/usr/bin/env bash
set -euo pipefail
CHANGE_ID="${1:?change id required}"
RECEIPT_DIR="receipts/${CHANGE_ID}"
mkdir -p "$RECEIPT_DIR"

# Record what the repo looked like before the generated change touched it.
git status --porcelain=v1 --untracked-files=all > "$RECEIPT_DIR/pre.status"

# Hash the prompt and all generated files so a later review can reproduce the input.
sha256sum "prompts/${CHANGE_ID}.md" > "$RECEIPT_DIR/hashes.txt"
find "generated/${CHANGE_ID}" -type f -print0 | sort -z | xargs -0 sha256sum >> "$RECEIPT_DIR/hashes.txt"

# Write the receipt skeleton.
jq -n \
  --arg id "$CHANGE_ID" \
  --arg created "$(date -u +%FT%TZ)" \
  --arg source "monkeycode-free-model" \
  '{change_id:$id, source:$source, created_at:$created}' > "$RECEIPT_DIR/receipt.json"

echo "receipt created at $RECEIPT_DIR/receipt.json"
Enter fullscreen mode Exit fullscreen mode

This script is intentionally small. It does not run the generated code; it records facts you can check later.

Stage 1: static deny-lists and syntax checks

With the receipt in place, run the generated files through a deny-list before you let them touch a server. Free models often repeat dangerous patterns because those patterns appear frequently in training data. Use grep to fail fast on the patterns that matter for your environment.

#!/usr/bin/env bash
set -euo pipefail
CHANGE_DIR="${1:?generated files dir}"
DENY_PATTERNS=(
  '0\.0\.0\.0/0'
  'chmod[[:space:]]+777'
  'curl[[:space:]]+.*\|[[:space:]]*bash'
  'privileged[[:space:]]*:[[:space:]]*true'
)
for pattern in "${DENY_PATTERNS[@]}"; do
  if grep -RInE "$pattern" "$CHANGE_DIR" --include='*' 2>/dev/null; then
    echo "deny-list match: $pattern" >&2
    exit 1
  fi
done
Enter fullscreen mode Exit fullscreen mode

Run the same checks on both the generated directory and the planned destination. A model may omit a file from its diff summary while still leaving an old dangerous file in place. After the deny-list, use the appropriate validators for the file types you actually have: terraform fmt -check -recursive, python -m json.tool, yamllint, shellcheck, or docker compose config. The validators do not prove the change is correct; they only prove it is well-formed.

Stage 2: no-network smoke test with a hard-link snapshot

Before you deploy to the free server, run the generated change in a network-isolated namespace. This catches changes that try to dial out, write outside the target directory, or rely on a missing local service.

Create a cheap snapshot with hard links. A hard-link snapshot costs almost no disk space, which matters on a small free server, but do not modify the original files while the snapshot exists: in-place edits will affect both paths because they share the same inode data.

CHANGE_ID="2026-08-17-sg-allowlist"
SNAPSHOT="snapshots/${CHANGE_ID}.$(date +%s)"
cp -al "generated/${CHANGE_ID}" "$SNAPSHOT"

# Run a validation script inside a network-isolated namespace.
# This will fail if the generated files try to reach the network.
unshare -n --map-root-user bash -c "ip link set lo up && cd '$SNAPSHOT' && ./validate.sh"
Enter fullscreen mode Exit fullscreen mode

If your free server does not give you root or unshare privileges, run the same check locally before uploading. Replace the placeholder validation with your own smoke script: start the service, send one local request, assert the expected file permissions, and stop the service.

Record the smoke-test exit code in the receipt:

jq '.sandbox_exit = 0' "receipts/${CHANGE_ID}/receipt.json" > tmp.json && mv tmp.json "receipts/${CHANGE_ID}/receipt.json"
Enter fullscreen mode Exit fullscreen mode

Stage 3: canary apply with an automatic rollback timer

The final stage promotes the change to the free server, but only after a canary path proves itself. The trick is to schedule a rollback before you apply, not after. If the canary fails silently, the timer restores the previous state without you having to be present.

#!/usr/bin/env bash
set -euo pipefail
CHANGE_ID="${1:?change id required}"
LIVE_DIR="${2:?live dir}"
CANARY_DIR="${3:?canary dir}"
ROLLBACK_AFTER_SECONDS="${4:-300}"

# Create a pre-change hard-link snapshot of the live directory.
cp -al "$LIVE_DIR" "${LIVE_DIR}.pre-${CHANGE_ID}"

# Apply the generated files to the canary path.
rsync -a --delete "generated/${CHANGE_ID}/" "$CANARY_DIR/"

# Schedule an automatic rollback unless a keepalive file is created.
(
  sleep "$ROLLBACK_AFTER_SECONDS"
  if [[ ! -f "/tmp/${CHANGE_ID}.keepalive" ]]; then
    rm -rf "$CANARY_DIR"
    mv "${LIVE_DIR}.pre-${CHANGE_ID}" "$LIVE_DIR"
    echo "rollback triggered for $CHANGE_ID" >&2
  fi
) &
rollback_pid=$!

# Run canary checks here. For example:
# curl -fsS http://127.0.0.1:8080/health || exit 1
# test "$(stat -c %a /srv/app/secret)" = "640" || exit 1

# If all checks pass, keep the canary and promote it.
touch "/tmp/${CHANGE_ID}.keepalive"
kill "$rollback_pid" 2>/dev/null || true
mv "$LIVE_DIR" "${LIVE_DIR}.old-${CHANGE_ID}"
mv "$CANARY_DIR" "$LIVE_DIR"
Enter fullscreen mode Exit fullscreen mode

This is a proposal, not a production rollout tool. The mv promotion assumes the live and canary directories are on the same filesystem so the rename is atomic. If your free server exposes object storage instead of a POSIX filesystem, replace the cp -al and mv steps with your provider's copy and versioning commands.

What this will not catch

The receipt-first preflight catches unreported deletions, permission changes, dangerous patterns, and network attempts during a smoke test. It will not catch logic bugs that only appear under production traffic, data races, dependency changes that happen later, or stateful migrations that corrupt a database. It also will not catch a model that writes a plausible explanation but generates a different file; the hash and repo status give you a way to notice that, but only if you compare them before reviewing the diff.

Hard-link snapshots share inode data. If you edit a file in place while the snapshot exists, the snapshot sees the same change. Use copy-on-write snapshots, a git commit, or an object-store version if you need true point-in-time isolation.

Who should skip this workflow

You should skip the timer-based canary promotion if you work on a team where multiple people can deploy to the same path, if your change touches a production database, or if your compliance rules require immutable backups instead of best-effort rollbacks. In those environments, use the receipt and deny-list stages as a pre-review step, then route the actual promotion through a normal CI/CD pipeline with manual approval.

If you already use a commercial static-analysis tool, keep it. The receipt is not a replacement for review; it is a cheap way to make free-model output auditable before you spend minutes or disk space on a free server.

A free model access option and a free server option lower the cost of experimenting with this workflow. Start with a single generated config change, create the receipt, run the deny-list, and promote through the canary timer. The receipt file makes the experiment repeatable, and repeated experiments are the fastest way to learn which failure modes actually affect your stack.

Top comments (0)