In July 2025, a developer told his AI agent eleven times, in ALL CAPS, not to touch production.
It deleted the database anyway.
The Replit agent wiped months of work, fabricated data to cover its tracks, and when asked to rate its own handling of the situation, gave itself a 95/100 on the data catastrophe scale.
This isn't isolated. Developers across the community are actively asking how to stop agents from going rogue in production.
The answer isn't always better instructions. Sometimes it's a simple checkpoint: ask a human first.
One way to do this: A human-in-the-loop approval system built using email. The agent pauses, sends an approval email, and waits. One tap from your inbox and it knows whether to proceed or not. No new tools. Just your inbox.
The Scenario
There's an agent monitoring support tickets. During one of its runs, it identifies some users hitting the same authentication bug introduced in the last deployment and decides to email all of them with a workaround.
Sounds helpful. But what if the workaround is wrong? What if it emails the wrong users? Without any guardrails, it would just do it.
Instead, it pauses and sends you an approval email with just enough context to make the call:
Action: Send workaround email to 12 affected users
Bug: Authentication failure on login after v2.3.1 deployment
Sample affected users: john@acme.com, sara@corp.io, mike@startup.com
Message that will be sent: "We've identified an issue affecting your account after our recent update. As a temporary workaround, please clear your browser cache and log in again."
You spot-check a name or two, and tap Approve or Reject right from your inbox. Nothing happens until you decide.
Architecture Overview
Here's the full flow:
Agent detects users hitting the same bug
↓
Server generates two signed URLs (approve + reject)
↓
Resend sends an approval email with context and two buttons to the approver
↓
Approver taps Approve or Reject from their inbox
↓
Next.js API route verifies the signature and checks expiry
↓
Decision is recorded, agent proceeds or stops
↓
Confirmation email fires, loop closed
Three moving parts:
The agent → detects the issue and triggers an approval request instead of acting immediately.
The approval email → built with React Email, sent via Resend, contains two buttons backed by signed URLs. This is the only thing the approver ever sees.
The Next.js API routes → generate the signed URLs, send the email, handle the click, and update the agent's state.
Prerequisites
A Resend account with a verified domain
A Groq account for a free API key
Setting Up the Project
git clone https://github.com/Calm-Rock/agent-email-approval.git
cd agent-email-approval
npm install
Create a .env.local file in the root by using .env.example as a reference:
RESEND_API_KEY=re_xxxxxxxxx
SECRET_KEY=your_key # generate with: openssl rand -hex 32
APPROVAL_BASE_URL=http://localhost:3000
APPROVER_EMAIL=you@youremail.com
FROM_EMAIL=agent@yourdomain.com
GROQ_API_KEY=your_groq_api_key_here
RESEND_API_KEY→ Your Resend API key.SECRET_KEY→ A random secret string used to sign the approve and reject URLs. This makes sure nobody can forge a click. You can generate one by runningopenssl rand -hex 32in your terminal.APPROVAL_BASE_URL→ The base URL of your Next.js server. During development this will behttp://localhost:3000. When you deploy, replace this with your actual domain.APPROVER_EMAIL→ The email address that will receive the approval request. It can be your personal Gmail, work email, anything.FROM_EMAIL→ The email address the agent sends from. This must be a verified domain on your Resend account.GROQ_API_KEY→ Your Groq API key for ticket analysis. Get a free key here.
Here's how the project is laid out:
agent-email-approval/
├── emails/
│ └── ApprovalEmail.jsx
├── app/
│ └── api/
│ ├── request-approval/
│ │ └── route.jsx
│ ├── approval/
│ │ └── route.js
│ └── approval-status/
│ └── route.js
├── utils/
│ ├── tokens.js
│ └── store.js
├── agent.js
├── tickets.json
└── .env.local
These are covered in the sections that follow.
The Approval Email
The approval email is built with React Email. It carries just enough context for the approver to make an informed decision: the bug, how many users are affected, a sample of who's impacted, and the exact message that will be sent to them.
The two buttons, Approve and Reject, are backed by signed URLs.
But why buttons? Why not just let the approver reply with 'APPROVE' or 'REJECT'?
One tap and you're done. No typing, no formatting, no opening a reply window.
Reply parsing is fragile and error-prone. Email clients add signatures, quote previous messages, and format text differently.
Replies can be spoofed. A signed URL is cryptographically tied to the action and expires after 24 hours.
The signed URL is the authentication. The approver doesn't need to log in anywhere. Clicking the button is the proof they have the right to decide.
You can find the full React Email template code at emails/ApprovalEmail.jsx.
The Approval Flow
This is where everything connects. When the agent detects an issue, it hits /api/request-approval. That endpoint generates two signed URLs, one to approve and one to reject, and fires off the approval email via Resend.
Signing the URLs
utils/tokens.js handles this. Each URL contains the action ID, the decision, and an expiry timestamp, all signed with HMAC-SHA256 using your SECRET_KEY. Verification uses timingSafeEqual instead of a regular string comparison to prevent timing attacks.
Sending the Email
app/api/request-approval/route.jsx takes the action details, generates the signed URLs, and sends the approval email via Resend.
Handling the Click
app/api/approval/route.js does four things in order: verifies the signature, checks expiry, guards against double clicks, and records the decision. Once recorded, it fires a confirmation email back to the approver and returns a response the approver sees in their browser.
The Agent
The agent is a standalone Node.js script, agent.js, that runs separately from the Next.js server. It communicates with the server entirely over HTTP.
Here's what it does in order:
Loads tickets from
tickets.json, a set of sample support tickets used to simulate real incoming requests.Sends them to Groq for analysis to determine which users are affected, what the bug is, and what workaround message to send
Hits
/api/request-approvalwith the analysis and waitsPolls
/api/approval-statusevery 3 seconds for a decision. The agent has no way of knowing when the approver has clicked, so it keeps checking the server until it gets an answer. In production, you could flip this around and have the server notify the agent directly via a webhook once the decision is recorded.If approved, sends the workaround email to every affected user via Resend
If rejected, stops
To run it, start the Next.js server first:
npm run dev
Then in a separate terminal run the agent:
node agent.js
NOTE: In Resend's free plan, you are limited to 2 requests per second. The agent already includes a 600ms delay between sends to handle this.
Here's what it looks like when you run it:
Once the agent finishes, you can check your Resend dashboard to see the delivery statuses for each email.
You can find the full agent script at agent.js and the sample tickets at tickets.json.
What This Protects Against
This setup doesn't make your agent smarter. It makes its mistakes catchable before they cause damage. Specifically, three things:
Incomplete reasoning: The agent decided on a workaround, but it doesn't know if it's correct, worded right, or targeting the right users. You do.
Irreversible actions: Emails can't be unsent. A ten second checkpoint is cheaper than a mass email apology.
Forged or replayed clicks: The token is tied to a specific action ID, expires after 24 hours, and can only be used once. A forged click gets rejected.
One limitation worth knowing: this is only as secure as the approver's inbox.
Where to Take It Next
Replace the in-memory store with a database: Decisions reset on every server restart. Swap utils/store.js for a proper database and you get persistence and an audit trail.
Multi-stakeholder approvals: Require two out of three approvers before the agent proceeds.
Timeout handling: Add a timeout so the agent stops or escalates if no decision is made within X hours.
Webhook instead of polling: Have the server notify the agent directly once a decision is recorded, instead of polling every 3 seconds.
Conversational approvals: Resend just launched a Chat SDK that turns email into a two-way channel. The approver could ask follow-up questions before making a final call.
Conclusion
The Replit agent deleted a production database, fabricated data to cover its tracks, and rated its own handling a 95 out of 100 on the catastrophe scale. Not because it was told the wrong thing. Because nothing stopped it from acting before a human could intervene.
That's the gap this fills. Not smarter instructions, not better prompts. Just a checkpoint before the action runs. One email, two buttons, the agent waits for a human to decide.
Email isn't the only way to put a human in the loop. But it's about as universal as it gets.
The full code is available on GitHub.





Top comments (0)