Pushary
Blog
Engineering

Fail-closed by default: why agent approvals should block instead of fire and forget

An agent approval that assumes yes on timeout is an approval in name only. The case for fail-closed defaults, and what it takes to implement them.

AG
Aadil Ghani
Founder, Pushary
Jul 17, 20263 min read
Share

Every approval system has to answer one design question before any other: when nobody answers, what happens? Fail-open says the work continues. Fail-closed says it stops. For AI agents holding real capabilities, fail-closed is the only defensible default, and the reasons are less philosophical than they look.

Key takeaways

  • The failure case for approvals is ordinary, not exotic: a phone in a pocket, a message that scrolled away. A fail-open gate turns each of those into consent.
  • The asymmetry decides the default: wrongly stopping costs a retry, wrongly proceeding costs whatever the approval was protecting.
  • Fail-closed takes infrastructure, not intentions: definite outcomes, a durable wait, and a record. A fire-and-forget message with an approve button cannot deliver it.

The ordinary failure

Fail-open designs implicitly assume the human saw the request and chose not to object. Consider how the request actually travels: a push notification during a meeting, a Slack message under forty others, an email in the morning pile. The person did not decline. They never knew.

Safety engineering settled this vocabulary long ago. A gate that fails open lets everything through when the mechanism breaks. For agent approvals, the mechanism "breaking" is not a rare fault to engineer around later. It is the median Tuesday.

The asymmetry that decides it

Approvals are placed on the specific actions where a mistake is expensive: moving money, deleting records, deploying, emailing a customer. That placement is exactly what makes the default calculable.

When a fail-closed gate is wrong, the cost is a delay. The agent reports it could not get approval, the person answers later or adjusts the policy, the action runs an hour late.

When a fail-open gate is wrong, the cost is the event itself: the refund issued, the table dropped, the email sent. There is no undo path priced into that branch, which is why the approval existed.

A default should absorb the cheap error to prevent the expensive one. That is the whole argument, and it does not depend on how reliable your delivery is on a good day.

Blocking is a feature, not a limitation

Fire-and-forget feels efficient: notify, continue, reconcile later. For decisions, "later" is the problem. The agent has already acted by the time anyone reconciles anything, so the human's role has quietly changed from approver to witness.

A blocking ask keeps the sequencing honest. The agent stops at the gate, the question travels, the person decides, the agent proceeds or does not. Blocking does not have to mean a process burning CPU on a wait, either. With the pending decision in a durable ledger, the wait can be a poll that any worker resumes, or a parked workflow that a webhook wakes, which is how long waits stay cheap. What must block is the action, not necessarily the process.

What it takes to implement

Three properties separate a fail-closed system from a fail-closed intention.

Definite outcomes. Every request resolves to approved, declined, or expired. There is no fourth state where the caller shrugs and picks a default at the call site, because call sites are exactly where discipline erodes.

A durable wait. The pending request lives in storage. A crash, a deploy, or a serverless timeout cannot orphan it into an unrecorded maybe.

A record. Every resolution, including the expiries, is written down. The expired entries are not noise. They are the log of every time the system chose safety, and the input for tuning windows and policies.

This is how Pushary's decisions.ask() behaves out of the box: it blocks until a person approves, declines, or answers, reports anything short of explicit approval as not approved, and writes every resolution to the decision ledger. The framework adapters inherit the default, so a Vercel AI SDK tool, a LangGraph node, and a CrewAI tool all stop on silence without any of them re-implementing the policy.

The default is the design

Timeout behavior looks like a configuration detail, one enum among many. It is closer to the constitution of the system: it decides what happens on every path you did not foresee. Fail-closed makes the unforeseen paths safe by construction, which is the most leverage a single default will ever buy you.

Frequently asked questions

What does fail-closed mean for an AI agent approval?

If nobody answers before the window closes, the decision resolves as denied and the agent does not take the guarded action. Silence, a missed notification, and a crash all land on no, never on yes.

Why not fail open and keep the agent moving?

Because the approval exists exactly for the actions where being wrong is expensive: refunds, deletions, deployments, outbound messages. For those, wrongly stopping costs a retry, while wrongly proceeding costs the thing the approval was protecting. The asymmetry decides the default.

What does a fail-closed implementation require?

A decision primitive with definite outcomes (approved, declined, expired), a durable wait that survives restarts so a crash cannot orphan the request, and a record of every resolution. An if statement around a notification does not get you there.

AG
Aadil Ghani
Founder, Pushary

Building Pushary so an AI agent can reach you on your phone and wait for a yes before it does something you would not want.

Read next

Engineering

Build vs buy: the real cost of a human-in-the-loop approval loop

The prototype takes a weekend. The system that will not silently approve a refund takes a quarter. An honest itemization, including when building is right.

Jul 17, 20263 min readAadil Ghani
Engineering

The gap between notifying a human and getting a decision back

Both start with a push message. The difference is everything after the tap, and it is the difference between an agent that announced and an agent that asked.

Jul 17, 20263 min readAadil Ghani
Engineering

Stop making your agent ask permission for everything. Or nothing.

Reads run free. The irreversible slice is small, and it is the only thing that should be allowed to interrupt you.

Jul 4, 20265 min readAadil Ghani

Get a push the moment your agent needs you

Approvals, done alerts, and a kill switch for Claude Code, Codex, Cursor, and the rest. It takes a couple of minutes to set up.