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"]
}
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
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.
- Commit
stack_contract.jsonwith the stack that is already in production, not the stack the model prefers. - Add an empty
ASSUMPTIONS.mdtemplate and keep it under version control. - Point the coding model at one ticket. Instruct it to edit
ASSUMPTIONS.mdfirst and to leave application files untouched until the assumptions parse. - Run the gate on
ASSUMPTIONS.md. If the file lists a new vendor or a new env key, stop. Do not generate code. - Allow a patch only after the assumptions file is valid. Then run the same gate on
git diff --cached. - 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.
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())
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
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
# 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
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
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)