VicroCode
Make Code Create Value
VicroCode is a lightweight online platform for publishing, running, sharing, and monetizing code projects. Launch HTML, Python, SQLite, AI agents, management tools, games, and more without server setup.
Please wait while VicroCode loads. You can also explore the AI programming guide
Loading...

AI MARKET GUIDE

Don't Trust the Agent's Auto Mode: Building an Editable Approval Gate With a SQLite Decision Log

A build debrief on replacing an agent's built-in auto-approval with a small policy engine that classifies each tool call, logs every decision, and holds risky ones for a human.

The thing that keeps coming up when people talk about running coding agents unattended is the same worry, phrased two ways. One builder shipping an open-source agent executor called zcode-executor described the core tension bluntly: handing an expensive model the keys is great for throughput, but let it run loose and it writes disaster code. Their answer was a Gate that borrows Claude Code's auto-mode idea, waves through routine operations, and automatically blocks sensitive file reads/writes and irreversible shell actions until a human releases them. Separately, OrcaTerm's latest client added task-state notifications that fire when a background agent task completes, fails, or is *waiting for user confirmation*.

Same shape, different products. Both admit that "auto" can't mean "unsupervised." The interesting part is that the approval logic and the human-review surface are the two pieces you'd want to own yourself, because they encode your risk tolerance, not the vendor's. So I rebuilt that idea as a small, standalone policy layer I could inspect and edit, rather than a mode baked into someone else's binary.

Why pull the gate out of the agent

The zcode-executor post also floated a headline number: stacking cheap-model triage with billing discounts turned a 5x quota into 20x productivity. Treat that as unverified. It's one person's account of their own setup, not a measured benchmark, and productivity multipliers are exactly the kind of claim that evaporates under scrutiny. What survives is the mechanism, not the multiplier: cheap work runs freely, dangerous work stops and waits.

When the gate lives inside the agent, you get a boolean you can't reason about. Which calls were auto-passed last night? Why did that `rm` get through but a config edit got held?

If you can't answer that, you don't have a safety layer, you have a vibe. Pulling the classifier out and giving every decision a durable record turns "the agent seemed fine" into something you can actually audit.

The three moving parts

The design is deliberately small. A classifier takes a tool call and returns a verdict. A log stores every verdict, forever. A review page shows what's held and lets a human release or reject it. That's it.

The classifier is plain Python, which fits neatly if you want to run Python online as a hosted endpoint the agent calls before it executes anything. The interface is boring on purpose: in comes a tool name and its arguments, out comes `auto_pass` or `hold`, plus the rule that fired and a one-line reason.

import re

# Rules are ordered; first match wins. Keep them boring and explicit.
HOLD_RULES = [
    ("irreversible_shell", re.compile(r"\b(rm|rmdir|dd|mkfs|truncate)\b")),
    ("force_push", re.compile(r"git\s+push\b.*(--force|-f)\b")),
    ("secrets_read", re.compile(r"\.(env|pem|key)$|credentials|secrets")),
    ("db_drop", re.compile(r"\b(DROP|TRUNCATE)\s+(TABLE|DATABASE)\b", re.I)),
]

def classify(tool_name: str, payload: str):
    target = f"{tool_name} {payload}"
    for rule_id, pattern in HOLD_RULES:
        if pattern.search(target):
            return {"verdict": "hold", "rule": rule_id,
                    "reason": f"matched {rule_id}"}
    return {"verdict": "auto_pass", "rule": "default_allow",
            "reason": "no hold rule matched"}

A regex table is not glamorous, and that's the point. You want the rules readable by a human who's tired and skimming at 6pm, because that's the person the gate protects. Anything fancier (parsing shell into an AST, scoring risk with a model) is a later optimization, and every layer of cleverness is a layer you have to trust when it decides to auto-pass.

The decision log is the actual product

Every call, passed or held, gets a row. SQLite is the right size for this: single file, no server, transactional, and easy to open later.

CREATE TABLE decisions (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  ts TEXT NOT NULL DEFAULT (datetime('now')),
  tool_name TEXT NOT NULL,
  payload TEXT NOT NULL,
  verdict TEXT NOT NULL,        -- auto_pass | hold
  rule TEXT NOT NULL,
  reason TEXT,
  status TEXT NOT NULL DEFAULT 'pending',  -- pending | approved | rejected
  reviewer TEXT,
  reviewed_ts TEXT
);

The log does double duty. Held actions sit as `pending` until a human touches them, and auto-passed actions still land as immutable history. When something goes sideways a week later, you're not reconstructing what happened from chat scrollback, you're running a query. And because it's a normal database file, when you need to fix a mislabeled rule or annotate a decision after the fact, you can open it in a SQLite editor and edit the row directly instead of writing a one-off migration script. That flexibility matters more than it sounds: policy tuning is iterative, and being able to eyeball and correct the log by hand keeps the feedback loop tight.

One discipline worth keeping: don't rewrite verdict history to make the log look cleaner. Correct metadata, sure, but the record of what the classifier actually decided is the thing you're keeping honest.

Surfacing held work for a human

The last piece is the part OrcaTerm's notifications hint at: someone has to notice the held action and decide. A queue that nobody looks at is just a slower way to block yourself. So the review surface is a plain HTML page listing `pending` rows, each with an approve and reject button that flips `status` through a small hosted endpoint.

Because it's a static-ish page talking to an in-platform endpoint, you can run HTML online and share the URL with whoever's on call, rather than standing up a separate web stack for what is essentially a to-do list with two buttons. The page polls the pending count, so a held action shows up without anyone babysitting a terminal.

Worth flagging plainly, since it's easy to skip: an approval endpoint that anything can POST to is a hole straight through your safety layer. If this is reachable beyond your own machine, put authentication in front of the approve/reject route before you wire it to a real agent. A gate with an open release button is not a gate.

What this buys you, and where it stops

The honest scope: this doesn't make an agent smarter or faster, and it won't turn 5x into 20x. What it does is convert an opaque "auto" toggle into a layer you can read, edit, and query. Routine calls flow. Risky calls stop and wait for a name to sign off. Every choice leaves a trail.

A few limits are baked in and worth stating. Regex rules catch what you've thought of; a novel dangerous command that doesn't match a pattern will auto-pass, so the default-allow posture is a deliberate risk you're accepting for throughput. If you'd rather fail closed, invert it to default-hold and whitelist the safe operations instead, at the cost of more human clicks. And the wider account here is built from public forum posts, not a controlled study, so treat the surrounding productivity narrative as directional rather than proven.

The part that holds up is the separation of concerns: keep the classifier, the log, and the review queue as three things you own, and "let the agent run" stops being an act of faith.