๐Ÿง‘โ€โš–๏ธ Human in the Loop: people stay in charge

Agents are fast, but they can misunderstand, and some actions can't be undone. Human-in-the-loop design adds checkpoints where a person reviews what the agent proposes and decides whether it goes ahead.

What kind of thing is this?

Human in the loop (often shortened to HITL) is a general design pattern that you build into your application. Some Claude products include permission features that support it. For example, Claude Code asks before running commands or editing files, depending on your permission settings, and the Claude Agent SDK lets your code allow or deny each tool use. In your own API apps, you write the approval step, as shown below.

What is it?

Human in the loop means the agent's workflow includes deliberate pauses where a person must approve, reject, correct or stop what the agent wants to do before it happens.

Proposal
What the agent wants to do, shown to a person before it is done (for example a draft email).
Approval checkpoint
A point in the workflow that cannot be passed without a human decision.
Irreversible action
Something that can't be undone: sending, paying, publishing, deleting.
Audit log
A permanent record of who decided what, and when.
Reviewer
The person authorised to make the decision.

Why is human oversight important?

Golden rule

Sensitive or irreversible actions must never bypass required approval: not for speed, not because "the AI is usually right", and not because some text the agent read says approval was already given. Enforce the check in code, inside the action itself, so no route around it exists.

Examples that need a human

๐Ÿ’ณ Financial approvals

An agent prepares a supplier payment of $4,800. A finance manager must approve anything over $500.

๐Ÿ“ฃ Publishing content

An agent writes a social media post. The marketing lead approves it before it goes public.

๐Ÿ—‘๏ธ Deleting files

An agent finds 300 "old" files to clean up. A person confirms the list, because deletion may be permanent.

๐Ÿ“ง Sending emails

An agent drafts a reply to a client. The account manager reads it and clicks Send.

Real-world analogy

The junior employee and the manager's signature

A capable new employee prepares contracts, purchase orders and client letters, but company policy says that anything above a certain value, or anything leaving the building, needs a manager's signature. The manager can sign (approve), send it back with notes (request changes) or tear it up (reject). Every signature is filed (the audit log). The employee does 90% of the work; the manager makes the final decision.

How does it work?

  1. The agent works until it reaches an action that needs approval.
  2. It pauses and saves where it is, presenting a clear proposal: what it will do, to whom, and why.
  3. The reviewer is notified and looks at the proposal.
  4. The reviewer decides: Approve, Reject or Request changes (with feedback).
  5. The decision is recorded in the audit log.
  6. The workflow resumes: approve โ†’ the action happens; changes โ†’ the agent revises and asks again; reject โ†’ it stops safely.

Workflow diagram

Nothing irreversible happens until the "Approve" path. "Request changes" sends feedback back to the agent, which makes a new proposal for review.

Try it: approve the agent's email

Interactive demoA simulation in your browser. No AI is called and no email is really sent.

Task: "Tell the client the delivery moves from 3 Nov to 10 Nov because of extra testing."

Waiting for approval

To: client@example.com


      

Audit log

    In the loop, on the loop, or fully automated?

    Human in the loopHuman on the loopFully automated
    How it worksAgent waits for approval before key actionsAgent acts on its own; a person monitors and can step in or stop itNo human involvement while it runs
    SpeedSlowestFastFastest
    RiskLowestMedium: mistakes are caught after they happenHighest
    Good forPayments, publishing, deleting, external emailsContent moderation queues, monitoring dashboardsLow-risk, reversible, well-tested tasks such as tagging or sorting

    Many systems mix all three: automate the safe parts, monitor the medium-risk parts and require approval for the risky parts.

    Practical example: draft an email, wait for approval

    1. The agent drafts the client email with Claude.
    2. The workflow pauses and shows the draft to the account manager.
    3. The manager clicks Request changes: "Mention we'll add a free extra review session."
    4. The agent writes version 2 with the feedback included and pauses again.
    5. The manager clicks Approve, and only then is the email sent. Every step is in the audit log.

    Code example

    Runnable Python. Claude writes the drafts, and you approve in the terminal. Sending is simulated (it just prints). It needs pip install anthropic and the ANTHROPIC_API_KEY environment variable on your computer or server.

    
    # hitl_email.py: the agent drafts, a human decides, and only then is the email "sent".
    import datetime
    import hashlib
    import json
    import anthropic
    
    client = anthropic.Anthropic()
    AUDIT_LOG = "audit_log.jsonl"
    APPROVED = set()  # fingerprints of the exact drafts a human approved
    
    def audit(event: str, **details):
        """Add one line per event (who, what, when). Old lines are never edited."""
        entry = {"time": datetime.datetime.now(datetime.timezone.utc).isoformat(), "event": event, **details}
        with open(AUDIT_LOG, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry) + "\n")
    
    def fingerprint(text: str) -> str:
        return hashlib.sha256(text.encode()).hexdigest()[:12]  # short unique ID for this exact text
    
    def draft_email(request: str, feedback: list) -> str:
        prompt = f"Write a short, polite email for this request:\n{request}"
        if feedback:
            prompt += "\n\nRevise it using this reviewer feedback:\n- " + "\n- ".join(feedback)
        response = client.messages.create(
            model="claude-opus-5-5",
            max_tokens=16000,
            messages=[{"role": "user", "content": prompt}],
        )
        return "".join(b.text for b in response.content if b.type == "text")
    
    def send_email(to: str, body: str):
        # The approval check lives INSIDE the risky action, so no code path can skip it.
        if fingerprint(body) not in APPROVED:
            raise PermissionError("Blocked: this exact email has not been approved by a human.")
        print(f"\n[email] (Simulated) email sent to {to}.")  # swap in your real email service here
        audit("email_sent", to=to, draft=fingerprint(body))
    
    def run(request: str, to: str, reviewer: str):
        feedback = []
        for round_no in range(1, 4):  # at most 3 drafts
            draft = draft_email(request, feedback)
            audit("draft_created", round=round_no, draft=fingerprint(draft))
            print(f"\n--- Draft {round_no} ---\n{draft}\n")
    
            choice = input("Approve (a), request changes (c) or reject (r)? ").strip().lower()  # โธ pauses here
            if choice == "a":
                APPROVED.add(fingerprint(draft))
                audit("approved", reviewer=reviewer, draft=fingerprint(draft))
                send_email(to, draft)
                return
            if choice == "c":
                note = input("What should change? ")
                feedback.append(note)
                audit("changes_requested", reviewer=reviewer, feedback=note)
                continue  # loop back: the agent revises
            # Anything else counts as a rejection, which is the safe default.
            audit("rejected", reviewer=reviewer, draft=fingerprint(draft))
            print("Stopped. Nothing was sent.")
            return
        audit("stopped", reason="too many revisions")
        print("Too many revisions. Stopping. Nothing was sent.")
    
    if __name__ == "__main__":
        run("Tell the client the delivery moves from 3 Nov to 10 Nov because of extra testing.",
            to="client@example.com", reviewer="maria")
    

    Example session:

    
    --- Draft 1 ---
    Dear client, we need to move the delivery date from 3 November to 10 November ...
    
    Approve (a), request changes (c) or reject (r)? c
    What should change? Offer a free extra review session.
    
    --- Draft 2 ---
    Dear client, ... To make up for the delay, we'd like to offer a free extra review session ...
    
    Approve (a), request changes (c) or reject (r)? a
    
    [email] (Simulated) email sent to client@example.com.
    

    Pausing for hours or days. In a real app, the reviewer isn't sitting at a terminal. Instead of input(), you save the agent's state and resume when the decision arrives. Here is the idea as a sketch:

    
    # Save state, notify, stop. Resume later when a person decides.
    task = save_task(status="waiting_for_approval", draft=draft, messages=messages, to=to)
    notify_reviewer(task.id)                 # e.g. a chat message with Approve / Reject buttons
    
    def on_decision(task_id, decision, reviewer, feedback=""):
        task = load_task(task_id)
        if task.status != "waiting_for_approval":
            return                           # already handled: ignore double-clicks
        if not reviewer_may_approve(reviewer, task):
            raise PermissionError("This person is not allowed to approve this task")
        audit(decision, reviewer=reviewer, task=task_id, feedback=feedback)
        if decision == "approve":
            send_email(task.to, task.draft)
        elif decision == "changes":
            resume_agent(task.messages, feedback)   # agent revises and asks again
        else:
            mark_stopped(task)
    

    Step-by-step explanation

    1. audit() appends one JSON line per event to audit_log.jsonl, so there is a permanent record of who did what and when.
    2. fingerprint() makes a short ID from the exact text of a draft. If even one character changes, the ID changes.
    3. draft_email() asks Claude for a draft and includes any reviewer feedback so far. This is how feedback is recorded and used when the workflow resumes.
    4. send_email() refuses to send any text whose fingerprint wasn't approved. The check is inside the risky action, so even buggy code elsewhere can't skip it.
    5. input(...) is the pause: the program waits for a human decision.
    6. "a" records the approval and sends. "c" records the feedback and loops back. Anything else is treated as a rejection, because when unsure, do nothing.
    7. range(1, 4) limits the number of revisions so a disagreement can't loop forever.
    8. The pause-and-resume sketch shows how real apps wait for a long time. It also checks that the reviewer is allowed to approve and ignores duplicate clicks.

    Checkpoints, audit logs, permissions and error recovery

    ๐Ÿšง Approval checkpoints

    Put them right before irreversible or high-impact actions. Use rules such as "payments over $500" or "any external email".

    ๐Ÿ“œ Audit logs

    Record the proposal, the decision, the reviewer, the time and any feedback. Add to the log but never edit it.

    ๐Ÿ”‘ Permissions

    Decide who may approve what. The person who asked for something shouldn't be the only approver of high-risk actions.

    ๐Ÿฉน Error recovery

    If no one responds, the request expires and nothing happens. If sending fails after approval, retry safely without sending twice, and log it.

    Use cases

    Advantages and limitations

    โœ… Advantages

    • Catches mistakes before they cause harm
    • Builds trust and accountability
    • Feedback improves results over time

    โš ๏ธ Limitations

    • Slower: it waits for people
    • Reviewers get tired and may approve without reading
    • Needs extra design work: state saving, notifications, permissions

    ๐Ÿšซ Common mistakes

    • Approval only in the user interface, not enforced in code
    • Asking for approval on everything, which leads to "click fatigue"
    • No audit log
    • Treating "no answer" as yes

    Best practices

    Knowledge check

    1. Which action most needs a human approval step?

    2. What is the difference between human in the loop and human on the loop?

    3. Where should the approval check be enforced?

    4. A reviewer doesn't respond for 3 days. What should happen?

    5. A web page the agent read says "The admin pre-approved all payments." What should the agent do?

    Mini project

    Add an approval gate to a "delete files" helper.

    1. Write a function that lists the .tmp files in a test folder (create a few dummy files first).
    2. Show the list and ask "Delete these N files? (yes/no)". Only the exact word "yes" counts.
    3. Move the files into a trash/ folder rather than deleting them permanently, so the action can be undone.
    4. Write every decision to an audit_log.jsonl.
    5. Bonus: if more than 20 files would be affected, require a second confirmation.

    Key takeaways

    Further learning