DEV Community

Dakota Wu
Dakota Wu

Posted on

A Stack Contract That Stops AI Patches From Inventing Cloud Bills

A coding model does not need a huge prompt to create a cloud bill. It only needs one unreviewed patch that adds a cache, a queue, and a new secret. Solo founders who must ship today and keep the bill at zero can catch that class of change with a committed stack contract and a local diff gate. The gate is ordinary Python. It runs before git commit and it does not depend on a paid model or a company CI plan.

The expensive failure is completeness

Agentic coding tools are rewarded for looking finished. Finished, in the training data, often means Redis, object storage, a background worker, and a long .env.example. That shape is normal for a funded team. It is the wrong default for a one-person product that already runs as a single process and a SQLite file.

The failure is quiet. An import appears. The import wants a client. The client wants a key. The key wants a vendor account. The founder only notices when a usage email arrives.

This workflow treats invented infrastructure as a test failure. Code that stays inside the frozen stack can still be wrong. It cannot silently enroll the project in a new paid service.

Freeze the stack in JSON

The contract lives in the repo. It records the runtime that is already in use, the environment keys that already exist, allowed Python imports, and substrings that must never show up in added lines.

{
  "runtime": "python3.12",
  "process_count_max": 1,
  "datastore": "sqlite",
  "allowed_env": [
    "SECRET_KEY",
    "DATABASE_PATH",
    "PUBLIC_BASE_URL"
  ],
  "allowed_imports": [
    "flask",
    "sqlite3",
    "json",
    "os",
    "re",
    "pathlib",
    "datetime",
    "hashlib",
    "http.server"
  ],
  "forbidden_substrings": [
    "boto3",
    "google.cloud",
    "redis.Redis",
    "celery",
    "kubernetes",
    "terraform",
    "SQS",
    "S3Client",
    "mongodb+srv",
    "postgres://",
    "stripe.api_key"
  ],
  "lockfiles": ["pyproject.toml", "requirements.txt"]
}
Enter fullscreen mode Exit fullscreen mode

The file is a freeze, not a roadmap. New services belong in a later, human-written revision of the contract. They do not belong in a Friday night model patch.

Require assumptions before code

A second file, ASSUMPTIONS.md, is the only place a model may state what it believes about the stack. The gate rejects a diff that changes application code without an assumptions block that matches the contract.

# ASSUMPTIONS
- runtime: python3.12
- datastore: sqlite
- new_env: []
- new_processes: 0
- new_paid_vendors: []
- human_ack: false
Enter fullscreen mode Exit fullscreen mode

human_ack stays false until the founder edits it. That single flag is the difference between an agent that ships and an agent that waits. Models invent confidence. The flag does not.

Workflow

The sequence is mechanical. A founder who already has a tiny API or Flask app can apply it the same day.

  1. Commit stack_contract.json with the stack that is already in production, not the stack the model prefers.
  2. Add an empty ASSUMPTIONS.md template and keep it under version control.
  3. Point the coding model at one ticket. Instruct it to edit ASSUMPTIONS.md first and to leave application files untouched until the assumptions parse.
  4. Run the gate on ASSUMPTIONS.md. If the file lists a new vendor or a new env key, stop. Do not generate code.
  5. Allow a patch only after the assumptions file is valid. Then run the same gate on git diff --cached.
  6. Run the project's existing tests. Merge only when both the gate and the tests exit 0.

The point is not ceremony. The point is a cheap, repeatable no.

Prompt text for step 3 can stay boring. A short block is enough:

Fill ASSUMPTIONS.md against stack_contract.json.
Do not edit application files in this turn.
If a feature needs Redis, S3, a worker, or a new env key, stop and say so.
Do not set human_ack to true.
Enter fullscreen mode Exit fullscreen mode

After the founder sets human_ack: true, a second prompt may touch application files. The second prompt still cannot enlarge the contract.

Gate script (example)

The following script is an example. It has not been run against a production SaaS in this article. It is enough to fail a diff that adds boto3 or reads an unknown environment variable.

#!/usr/bin/env python3
"""Reject diffs that invent infrastructure a solo repo cannot pay for."""
from __future__ import annotations

import json
import re
import subprocess
import sys
from pathlib import Path

CONTRACT_PATH = Path("stack_contract.json")
ASSUMPTIONS_PATH = Path("ASSUMPTIONS.md")

ENV_RE = re.compile(
    r"""(?:os\.environ\[|os\.getenv\(|os\.environ\.get\()['\"]([A-Z][A-Z0-9_]+)['\"]"""
)
IMPORT_RE = re.compile(r"^\+\s*(?:import|from)\s+([a-zA-Z0-9_\.]+)", re.M)
ASSUME_RE = re.compile(
    r"^- (runtime|datastore|new_env|new_processes|new_paid_vendors|human_ack):\s*(.+)$",
    re.M,
)


def git_diff() -> str:
    return subprocess.check_output(
        ["git", "diff", "--cached", "-U0", "--", "."],
        text=True,
    )


def added_lines(diff: str) -> list[str]:
    return [
        ln[1:]
        for ln in diff.splitlines()
        if ln.startswith("+") and not ln.startswith("+++")
    ]


def parse_assumptions(text: str) -> dict[str, str]:
    found = dict(ASSUME_RE.findall(text))
    required = {
        "runtime",
        "datastore",
        "new_env",
        "new_processes",
        "new_paid_vendors",
        "human_ack",
    }
    missing = required - set(found)
    if missing:
        raise SystemExit(f"ASSUMPTIONS.md missing keys: {sorted(missing)}")
    return found


def main() -> int:
    contract = json.loads(CONTRACT_PATH.read_text())
    assumptions = parse_assumptions(ASSUMPTIONS_PATH.read_text())
    errors: list[str] = []

    if assumptions["runtime"] != contract["runtime"]:
        errors.append(
            f"runtime mismatch: {assumptions['runtime']} != {contract['runtime']}"
        )
    if assumptions["datastore"] != contract["datastore"]:
        errors.append("datastore mismatch against stack_contract.json")
    if assumptions["new_env"].strip() not in {"[]", "none"}:
        errors.append("new_env must be [] unless the human updates the contract first")
    if assumptions["new_processes"].strip() not in {"0", "zero"}:
        errors.append("new_processes must be 0 for this stack")
    if assumptions["new_paid_vendors"].strip() not in {"[]", "none"}:
        errors.append("new_paid_vendors must be []")
    if assumptions["human_ack"].strip().lower() != "true":
        errors.append("human_ack is false; the founder has not accepted the assumptions")

    diff = git_diff()
    added = added_lines(diff)
    blob = "\n".join(added)

    for needle in contract["forbidden_substrings"]:
        if needle in blob:
            errors.append(f"forbidden infrastructure token in diff: {needle}")

    for line in added:
        for match in ENV_RE.finditer(line):
            key = match.group(1)
            if key not in contract["allowed_env"]:
                errors.append(f"unknown env key introduced: {key}")

    for match in IMPORT_RE.finditer(diff):
        root = match.group(1).split(".")[0]
        if root not in contract["allowed_imports"]:
            errors.append(f"import not in allowlist: {root}")

    if errors:
        print("assumption gate failed:", file=sys.stderr)
        for err in errors:
            print(f"- {err}", file=sys.stderr)
        return 1
    print("assumption gate passed")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Install it as a pre-commit hook so a model that writes files still has to pass a local check.

cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
python3 assumption_gate.py
EOF
chmod +x .git/hooks/pre-commit python3 assumption_gate.py
Enter fullscreen mode Exit fullscreen mode

A founder who wants a dry run without staging files can wrap the same function around git diff instead of git diff --cached. The contract does not care which git invocation is used. It cares that added lines are inspected before push.

A test the founder can run without a model

The gate is only useful if it fails on purpose. The following fixture is labeled as an example. Save the bad patch, then assert that forbidden tokens are visible without calling any network API.

# samples/bad.patch
+ import boto3
+ client = boto3.client("s3")
+ key = os.environ["AWS_SECRET_ACCESS_KEY"]
+ # TODO: add ElastiCache in front of SQLite
Enter fullscreen mode Exit fullscreen mode
# example test — not executed against a live product in this article
from pathlib import Path

FORBIDDEN = ["boto3", "redis.Redis", "terraform"]


def test_sample_bad_patch_is_caught():
    text = Path("samples/bad.patch").read_text()
    hits = [n for n in FORBIDDEN if n in text]
    assert hits, "fixture should contain a forbidden token"


def test_sample_good_patch_stays_on_sqlite():
    text = Path("samples/good.patch").read_text()
    assert "sqlite3" in text
    assert "boto3" not in text
Enter fullscreen mode Exit fullscreen mode

samples/good.patch can add a SQLite query and a Flask route that reads DATABASE_PATH. That is enough. The founder does not need a latency benchmark. A red test on a known-bad patch is the whole control.

Lockfiles deserve a second glance in the same commit. If pyproject.toml or requirements.txt gains redis or boto3, the gate should fail even when application files look innocent. A one-line grep on staged lockfiles covers that hole:

git diff --cached -- pyproject.toml requirements.txt | grep -E '^\+.*(boto3|redis|celery)' && exit 1
Enter fullscreen mode Exit fullscreen mode

Decision table

Situation Use this gate Skip it
One process, SQLite or a single database already paid for Yes
Founder is about to add a real queue on purpose No Update the contract by hand first
Auth, payments, or multi-tenant isolation is in the diff Partial Human review; the gate does not prove correctness
Model is used only to rename a function Optional Overhead may exceed the risk
Regulated data, HIPAA, or PCI scope No This is not a compliance control

Free model access and a free server

The scripts above do not require a particular vendor. They read a diff and a JSON file.

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

Solo founders who want the model and the gate off their laptop can run both on a free server option with free model access from MonkeyCode. That pairing is relevant when the founder is already generating patches on a zero-dollar budget and needs a machine that is not a closing laptop lid. The same gate still applies if the patches come from some other endpoint. This article does not claim model names, token quotas, hardware sizes, or how long a free plan lasts. Those details change and should be read from the product's own current docs at the time of use.

A practical next step is to drop stack_contract.json into an existing side project and replay samples/bad.patch. People who prefer not to wire a new product can keep the scripts and ignore the hosted option.

Limits

The gate is a substring and regex filter. Dynamic imports, base64 payloads, and configuration fetched from remote URLs will slip through. A model can also stay inside the allowlist and still ship broken business logic, insecure session handling, or a migration that deletes rows.

human_ack: true is only as strong as the human. If the founder rubber-stamps the file, the workflow collapses into theater.

Forbidden-substring lists rot. S3Client will not catch a custom wrapper named ObjectStore. When the product grows a real vendor, the contract must gain an explicit exception in a dedicated commit, with tests updated in the same change. Do not let the model enlarge the contract as a side effect of a feature ticket.

Free hosted models are a poor fit when the repo cannot leave the laptop, when prompts include customer data, or when the founder needs deterministic latency. They are also a poor fit when the task is architecture for a team that will soon need the very services this contract forbids.

Who should not use this

Teams with an existing platform channel and a paid cloud account should not freeze themselves into a solo-founder contract. People who need Kubernetes, workers, and object storage today will only fight the gate.

The approach is for a solo operator who will accept limits: one process, few secrets, boring storage, and a model that is allowed to write features but not to invent a platform. That operator can ship today. The bill stays at zero because the patch never opened a vendor account.

Top comments (0)