Weekend agent demos break when the model can mutate the working tree while it is still exploring. A small permit file and an on-disk outbox keep exploration cheap and commits explicit. The demo is not a longer chat. It is one flushed write that a test can reject.
This note is a weekend build log, not a platform review. Scope was cut first. The working path is a permit desk. Everything else was skipped on purpose.
The failure that wastes Saturday
Side-project agents do not usually fail on the first prompt. They fail on the third retry, when a “fix the demo” turn rewrites README.md, a config file, and a test in the same breath. The transcript still looks productive. The tree does not.
Read-only turns are cheap. Writes are the scarce resource. A weekend kit should treat them that way.
Recent public debate around “vibe coding” versus engineering is noisy. The practical split on a laptop is smaller. If a tool call can change disk, it needs a permit. If it cannot, it can loop until the clock runs out.
Scope cut for one weekend
The build kept three jobs and deleted the rest.
- Load a static allowlist of write paths from the repo.
- Catch attempted writes and append them to
outbox.jsonlinstead of touching the tree. - Flush one approved record through a command that a test can run twice.
Skipped on Saturday: multi-user auth, cloud queues, model routing, token dashboards, and any claim about which model is “best.” Those inflate the demo. They do not prove the permit.
The artifact is one Python module, one permit file, one test, and a short shell path. That is the whole ship.
The permit file
The allowlist is boring on purpose. Boring files survive a weekend.
{
"version": 1,
"demo_id": "weekend-outbox-001",
"write_roots": ["demo_out/"],
"max_flush": 1,
"forbidden_globs": [
".git/**",
"**/*secret*",
"**/.env",
"permits.json"
]
}
write_roots is the only place a flush may land. forbidden_globs is a hard veto even if a model argues. max_flush is the Saturday halt: one committed file, then stop. The agent may propose many writes. The desk keeps one.
The outbox desk
The module below is a complete, runnable sketch. It does not call a network model. It only gates writes. Label it as a local fixture, not a production agent runtime.
#!/usr/bin/env python3
"""permit_desk.py — catch writes, flush one permitted path."""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
PERMIT_PATH = ROOT / "permits.json"
OUTBOX_PATH = ROOT / "outbox.jsonl"
FLUSHED_PATH = ROOT / "flushed.json"
@dataclass(frozen=True)
class Permit:
version: int
demo_id: str
write_roots: tuple[str, ...]
max_flush: int
forbidden_globs: tuple[str, ...]
def load_permit(path: Path = PERMIT_PATH) -> Permit:
raw = json.loads(path.read_text(encoding="utf-8"))
return Permit(
version=int(raw["version"]),
demo_id=str(raw["demo_id"]),
write_roots=tuple(raw["write_roots"]),
max_flush=int(raw["max_flush"]),
forbidden_globs=tuple(raw["forbidden_globs"]),
)
def _is_forbidden(rel: str, permit: Permit) -> bool:
text = rel.replace("\\", "/")
for glob in permit.forbidden_globs:
pattern = re.escape(glob).replace("\\*\\*", ".*").replace("\\*", "[^/]*")
if re.fullmatch(pattern, text):
return True
return False
def _under_root(rel: str, permit: Permit) -> bool:
text = rel.replace("\\", "/")
return any(text.startswith(root) for root in permit.write_roots)
def propose_write(relpath: str, content: str, permit: Permit | None = None) -> dict[str, Any]:
permit = permit or load_permit()
rel = relpath.replace("\\", "/")
record = {
"ts": datetime.now(timezone.utc).isoformat(),
"demo_id": permit.demo_id,
"relpath": rel,
"bytes": len(content.encode("utf-8")),
"content": content,
"status": "queued",
}
if _is_forbidden(rel, permit) or not _under_root(rel, permit):
record["status"] = "rejected"
record["reason"] = "path not permitted"
OUTBOX_PATH.parent.mkdir(parents=True, exist_ok=True)
with OUTBOX_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
return record
def flush_one(permit: Permit | None = None) -> dict[str, Any]:
permit = permit or load_permit()
if FLUSHED_PATH.exists():
raise SystemExit("demo already flushed; refuse second write")
if not OUTBOX_PATH.exists():
raise SystemExit("outbox empty")
queued = []
for line in OUTBOX_PATH.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
item = json.loads(line)
if item.get("status") == "queued":
queued.append(item)
if not queued:
raise SystemExit("no queued writes")
if permit.max_flush != 1:
raise SystemExit("this weekend kit flushes exactly one record")
chosen = queued[0]
target = ROOT / chosen["relpath"]
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(chosen["content"], encoding="utf-8")
chosen["status"] = "flushed"
FLUSHED_PATH.write_text(json.dumps(chosen, indent=2), encoding="utf-8")
return chosen
The important behavior is negative. A rejected path still lands in the outbox. It never lands in demo_out/. The Saturday operator reads the log, not the model’s self-report.
A test that fails before the demo
The kit is not real until a test refuses a write the agent would love to make.
# test_permit_desk.py
from pathlib import Path
import json
import permit_desk as desk
def test_rejects_git_and_env(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "permits.json").write_text(json.dumps({
"version": 1,
"demo_id": "t",
"write_roots": ["demo_out/"],
"max_flush": 1,
"forbidden_globs": [".git/**", "**/.env"],
}))
monkeypatch.setattr(desk, "ROOT", tmp_path)
monkeypatch.setattr(desk, "PERMIT_PATH", tmp_path / "permits.json")
monkeypatch.setattr(desk, "OUTBOX_PATH", tmp_path / "outbox.jsonl")
monkeypatch.setattr(desk, "FLUSHED_PATH", tmp_path / "flushed.json")
bad = desk.propose_write(".env", "TOKEN=nope")
assert bad["status"] == "rejected"
assert not Path(".env").exists()
good = desk.propose_write("demo_out/hello.txt", "ok\n")
assert good["status"] == "queued"
flushed = desk.flush_one()
assert flushed["relpath"] == "demo_out/hello.txt"
assert (tmp_path / "demo_out/hello.txt").read_text() == "ok\n"
Run it locally:
python3 -m pip install pytest
python3 -m pytest -q test_permit_desk.py
The demo is the failing case as much as the passing one. If .env can be written, the weekend is already over.
Commands that prove one path
A short shell path is the public artifact. Chat logs are not.
python3 - <<'PY'
from permit_desk import propose_write, flush_one
print(propose_write("README.md", "# hijack\n"))
print(propose_write("demo_out/receipt.txt", "demo ok\n"))
print(flush_one())
PY
cat outbox.jsonl
cat flushed.json
cat demo_out/receipt.txt
test ! -f README.md
Expected shape of outbox.jsonl:
{"relpath": "README.md", "status": "rejected", "reason": "path not permitted"}
{"relpath": "demo_out/receipt.txt", "status": "queued"}
After flush, README.md is still absent. demo_out/receipt.txt exists. A second flush_one() exits non-zero. That second failure is part of the demo. It records the skip: no extra writes after the receipt.
Decision table for Saturday
| Attempted path | Permit result | Disk effect |
|---|---|---|
demo_out/receipt.txt |
queued, then one flush | file created |
README.md |
rejected | unchanged |
.env |
rejected | unchanged |
.git/config |
rejected | unchanged |
| second flush | process exit | no extra file |
The table is the product. Models can narrate. The table cannot.
Where a free remote host fits
The permit file has to travel with the repo. A local laptop and a remote workspace should load the same permits.json. If the host changes and the allowlist does not, the agent has been given a new machine without a new contract.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode’s free model access and free server option can host that same layout so the permit desk is not tied to one laptop. No model names, quotas, or hardware claims are attached here because they are not needed to run the fixture. The desk still refuses .env if the process is far away.
Readers who already have a box can ignore the host and keep the files. The workflow does not depend on a vendor remaining available.
What this weekend skipped
Skipped items were written down so they do not sneak back in as “small refactors.”
- No retry budget. The step count is a different kit.
- No spend meter. Token cost is a different kit.
- No golden stdout log. The receipt is a file on disk, not a captured stream.
- No merge gate for generated patches. Flush is not a pull request.
- No attempt to rank models. The allowlist does not care who proposed the write.
Those skips are features of the Saturday. A demo that also measures cost, steps, and model quality is no longer a demo. It is a platform.
Limits
The glob matcher is a short regex stand-in. It is not a full gitignore engine. Symbolic links, case-folded filesystems, and writes through subprocesses that never call propose_write will bypass it. The desk is an honor system plus a test, not a kernel sandbox.
max_flush is a file flag, not a distributed lock. Two shells can race. That is acceptable for a weekend side project. It is not acceptable for a shared production volume.
The outbox stores full file contents in JSONL. Large binaries do not belong there. Text receipts do.
Who should not use this approach
Teams with a real sandbox, mandatory code review, or existing policy engines do not need a JSON allowlist. People shipping secrets, payments, or medical data should not treat an outbox file as isolation. Operators who want a ranked model bake-off will not find one in this log.
The kit is for a single maintainer who already let an agent wander through a repo once and did not like the diff. It restores a boring rule: reads may loop, writes need a permit, and the weekend ends after one flushed path.
Top comments (0)