Building AI Agents from Scratch

Human Approval and Guardrails

Require confirmation before the agent sends, deletes, purchases, or modifies data.

Read-only tools can run. Anything that sends, pays, deletes, or overwrites needs a human in the loop. The model is not that human.

Mark dangerous tools

DANGEROUS = {"write_file", "send_email", "delete_file"}


def write_file(path: str, text: str, approved: bool = False) -> dict:
    if not approved:
        return {
            "ok": False,
            "error": "approval required",
            "preview": {"path": path, "bytes": len(text)},
        }
    target = (ROOT / path).resolve()
    if ROOT not in target.parents and target != ROOT:
        return {"ok": False, "error": "path escapes the project"}
    target.write_text(text, encoding="utf-8")
    return {"ok": True, "path": path}

Pause the loop

When a dangerous tool returns approval required, stop and print the preview. Read a yes/no from the terminal (or a UI). Only then re-invoke with approved=True from your code — do not let the model set that flag itself. Strip approved from the schema the model sees, or ignore it if present.

if name in DANGEROUS:
    preview = run_tool(name, arguments_json)
    print(preview)
    if input("Approve? [y/N] ").strip().lower() != "y":
        return json.dumps({"ok": False, "error": "human rejected"})
    args = json.loads(arguments_json or "{}")
    args["approved"] = True
    return json.dumps(REGISTRY[name](**args))

Check it

  1. Ask the agent to write notes.md. You should see a preview and a prompt. Answer n — the file must not exist.
  2. Answer y on a second try — the file exists with the previewed bytes.
  3. Confirm list_dir never asks.

Next: plug MCP servers into the same registry.