I keep a bad habit that every model tutorial quietly encourages, and I almost repeated it this week. Have you ever stared at a boring curl-and-jq pipeline and still refused to paste it locally? I spent forty-eight hours routing every proposed command through a quarantine runner on a free remote server. The runner did not make the model smarter, but it finally made the environment tell the truth.
Why my laptop kept lying
My laptop is a museum of leftover CLIs, virtualenvs, and aliases that no clean server will ever have. When a free model emits a shell script, it is guessing at a POSIX box I do not actually run. So of course the script exits zero on my machine; that success is autobiographical, not reproducible at all. What happens if the next machine is empty, with no jq, no rg, and a boring default PATH?
That empty machine is the point of a quarantine box. I wanted the command to fail in public, with a receipt, before it mutated a repo I actually care about. Local green was not evidence. Local green was a diary entry about my own junk drawer.
The forty-eight hour setup
I needed two cheap pieces for this loop, not a platform tour or a new identity.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. I used MonkeyCode's free model access to draft candidate commands, and I used the free server option as the quarantine host. I will not invent model names, quotas, hardware, or duration claims; the contract between prompt, argv, and receipt is the actual subject.
The loop I kept for two days looked like this:
- Ask the model for
argvas a JSON list, plusexpected_exitand a one-line rationale. - Refuse to pipe model stdout into
bashon the laptop, even when the command looks boring. - Copy a job file to the free server and execute it under timeout, cwd, and an environment allowlist.
- Write a receipt that stores argv, exit code, duration, and truncated stdout/stderr.
- Only then decide whether the command deserves to touch a real working tree.
Is that slower than pasting into a terminal? Yes. Is pasting into a terminal how sudo rm fan fiction gets a real inode? Also yes.
The artifact: a receipt printer, not a kernel
This is the runner I dropped on the box. Treat it as a receipt printer with a timeout, not as a sandbox kernel, because it is not one.
#!/usr/bin/env python3
"""quarantine_run.py — execute one job JSON and write one receipt JSON."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
from pathlib import Path
MAX_LOG_BYTES = 8_192
ALLOWED_ENV_DEFAULT = ("PATH", "HOME", "LANG", "LC_ALL", "TERM")
def clip(data: bytes, limit: int = MAX_LOG_BYTES) -> str:
text = data.decode("utf-8", errors="replace")
if len(text) <= limit:
return text
return text[:limit] + f"\n...[truncated {len(text) - limit} chars]\n"
def load_job(path: Path) -> dict:
job = json.loads(path.read_text(encoding="utf-8"))
argv = job.get("argv")
if not isinstance(argv, list) or not argv or not all(isinstance(x, str) for x in argv):
raise ValueError("job.argv must be a non-empty list of strings")
if any(part == "" for part in argv):
raise ValueError("job.argv contains an empty string")
job["timeout_sec"] = int(job.get("timeout_sec", 30))
job["expected_exit"] = int(job.get("expected_exit", 0))
job["cwd"] = job.get("cwd") or os.getcwd()
job["env_allowlist"] = list(job.get("env_allowlist") or ALLOWED_ENV_DEFAULT)
return job
def build_env(allowlist: list[str]) -> dict[str, str]:
env = {}
for key in allowlist:
if key in os.environ and os.environ[key] != "":
env[key] = os.environ[key]
# Never inherit secrets, tokens, or SSH agent sockets by accident.
env.pop("SSH_AUTH_SOCK", None)
return env
def run_job(job: dict) -> dict:
started = time.monotonic()
receipt = {
"job_id": job.get("job_id"),
"argv": job["argv"],
"cwd": job["cwd"],
"timeout_sec": job["timeout_sec"],
"expected_exit": job["expected_exit"],
"exit_code": None,
"timed_out": False,
"stdout": "",
"stderr": "",
"duration_ms": None,
"match_expected": False,
}
try:
completed = subprocess.run(
job["argv"],
cwd=job["cwd"],
env=build_env(job["env_allowlist"]),
capture_output=True,
timeout=job["timeout_sec"],
check=False,
shell=False,
)
receipt["exit_code"] = int(completed.returncode)
receipt["stdout"] = clip(completed.stdout)
receipt["stderr"] = clip(completed.stderr)
except subprocess.TimeoutExpired as exc:
receipt["timed_out"] = True
receipt["exit_code"] = None
receipt["stdout"] = clip(exc.stdout or b"")
receipt["stderr"] = clip(exc.stderr or b"") + "\n[timeout]\n"
except FileNotFoundError as exc:
receipt["exit_code"] = 127
receipt["stderr"] = f"FileNotFoundError: {exc}\n"
receipt["duration_ms"] = int((time.monotonic() - started) * 1000)
receipt["match_expected"] = (
not receipt["timed_out"] and receipt["exit_code"] == job["expected_exit"]
)
return receipt
def main() -> int:
if len(sys.argv) != 3:
print("usage: quarantine_run.py JOB.json RECEIPT.json", file=sys.stderr)
return 2
job_path = Path(sys.argv[1])
receipt_path = Path(sys.argv[2])
job = load_job(job_path)
receipt = run_job(job)
receipt_path.write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"job_id": receipt["job_id"], "match_expected": receipt["match_expected"], "exit_code": receipt["exit_code"]}))
return 0 if receipt["match_expected"] else 1
if __name__ == "__main__":
raise SystemExit(main())
A job file is deliberately boring. If the model cannot emit this shape, I do not run the command.
{
"job_id": "2026-09-03-lint-1",
"argv": ["python3", "-m", "compileall", "-q", "src"],
"cwd": "/home/ubuntu/work",
"timeout_sec": 30,
"expected_exit": 0,
"env_allowlist": ["PATH", "HOME", "LANG"]
}
On the box I ran the same two commands over and over, because muscle memory should live in a script, not in my history file.
python3 quarantine_run.py jobs/2026-09-03-lint-1.json receipts/2026-09-03-lint-1.json
echo $?
python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['exit_code'])" receipts/2026-09-03-lint-1.json
The prompt I sent the model was equally strict. I wanted argv, not prose with backticks that I would later retype wrong.
Propose one command as JSON only.
Keys: argv (list of strings), cwd, timeout_sec, expected_exit, rationale.
Do not wrap in markdown.
Do not use shell features: no pipes, no redirects, no sudo, no ssh, no curl-to-bash.
If the task needs a pipeline, say so in rationale and set argv to ["/bin/false"].
Would I trust that prompt alone? No. The runner is there because models still emit jq . as if jq were a human right.
What I tried
I started with compile checks, because they fail closed and they do not need network. Then I tried tiny file rewrites the model wanted to perform with sed, and I rejected every sed that was not already an argv list. Then I tried a linter the model assumed was on PATH. Then I tried a Python module the model assumed I had installed globally, which I had, on the laptop, because of course I had.
The useful part was not the tasks. The useful part was forcing every task through the same receipt shape. Once the receipt existed, I could diff two failures without arguing with a chat transcript that had already scrolled away.
What broke
Exit code 127 showed up first, and it kept showing up. The model wrote ruff, jq, and rg as if they were /bin/true. My laptop had all three. The free server had python3, git, and a shell, which is a different planet pretending to be the same OS.
Shebang lies showed up second. A script the model labeled run.sh had no #!/bin/sh line, so ./run.sh failed while sh run.sh worked. Should a quarantine box guess? I decided no, because guessing is how you get two greens that mean opposite things.
Working directory lies showed up third. The model kept assuming cwd was the repo root, then wrote paths like src/app.py from a home directory that contained no src. Locally I had already cd'd into the project, so the same argv looked brilliant. Remotely it looked like a missing file, which it was.
Timeouts showed up fourth, usually around anything that implied a package install. A free server is not your CI cache. A model that says "just pip install" is proposing a network novel, not a command. I started failing those jobs on sight when argv[0] was pip or pip3.
Environment inheritance showed up last, and it was the one that actually scared me. If I had used shell=True or passed env=os.environ.copy(), a token in my local session could have ridden along with a "harmless" formatter. The allowlist is not a vibe. The allowlist is the product.
Here is the short list I wish I had taped above the terminal:
-
127means the model invented a CLI that exists only in training data and on my laptop. - Missing shebang is not a style nit; it is two different programs.
- cwd must be in the job file, not in the chat memory of whoever ran it last.
-
shell=Falseplus argv lists kill pipes, globs, and$(reboot)fan fiction. - Truncate logs in the receipt, or one noisy compiler will bury the exit code.
- Never allow
sudo,ssh,curl, orwgetin argv[0] unless you are studying incidents.
A small decision table I actually used
| Receipt signal | What I assumed first | What I check second | What I do next |
|---|---|---|---|
exit_code = 127 |
model hallucinated a tool |
argv[0] on the server with command -v
|
install nothing; change the job |
timed_out = true |
model proposed a download | whether argv implies network | reject the job |
exit_code = 2 from python |
bad flags | whether cwd contains the module | fix cwd, do not reroll prose |
match_expected = true |
the patch is safe | whether stdout is empty for a reason | only then copy artifacts back |
stderr mentions Permission denied
|
the box is broken | whether the model wanted /usr
|
delete the job |
Notice what is missing from that table: vibes, screenshots of chat, and "it worked on my machine." Those are not signals. Those are how I got here.
What I would repeat
I would keep argv as a list, even when the model begs to write a one-liner with pipes. I would keep expected_exit in the job, because a command that "fails interestingly" is still a contract. I would keep receipts on disk, named after job_id, so forty-eight hours of noise can be grepped later.
I would also keep a tiny denylist beside the runner. It is not clever, and that is why it works.
DENY_ARGV0 = {"sudo", "su", "ssh", "scp", "curl", "wget", "dd", "mkfs", "reboot", "shutdown"}
def reject_job(job: dict) -> str | None:
head = Path(job["argv"][0]).name.lower()
if head in DENY_ARGV0:
return f"denied argv0={head}"
if job["timeout_sec"] > 60:
return "timeout too large for a quarantine loop"
return None
Would I repeat pasting model output into my own shell "just this once"? I would not. The one-liner is never the cost. The cost is the environment you forgot you personalized.
Limitations, and who should not use this
This runner is not gVisor, not a VM rollback, and not a substitute for code review. It still executes process arguments on a real Unix box that you control. A determined payload can still waste CPU, fill a disk, or read any file the server user can read. If you need isolation, you need an actual sandbox, not a JSON receipt.
Do not put secrets on a free shared server and then congratulate yourself for using an allowlist. Do not point cwd at a production checkout. Do not treat match_expected = true as a merge bit. Do not use this loop if your threat model includes untrusted users submitting argv lists; that is a remote execution product, and this article is not that product.
People who should skip this workflow:
- Anyone hoping the model will administer the box for them.
- Anyone without permission to run arbitrary processes on the host.
- Anyone replacing CI with a chat window and a timeout.
- Anyone who needs guaranteed hardware, quotas, or a named model card this article refused to invent.
Field notes I am keeping
The headline is still the whole lesson. The script exited 0 on my laptop because my laptop is a souvenir shop. The free server returned 127 because ruff was a rumor there. After forty-eight hours I trust receipts more than I trust fluency, and I trust argv lists more than I trust backticks.
If you already have a throwaway box, steal the runner, keep the receipts, and make the next "harmless" command fail somewhere that is not your working tree.
Top comments (0)