---
title: "The Approval Gate That Blocked Us for Fifteen Hours and Never Caught a Mistake"
canonical: https://dxdev.com/blog/approval-gate-blocked-me-fifteen-hours/
datePublished: 2026-09-16
---
We could not push a docs-only change for about fifteen hours, and in that time the guard blocking us never caught a real mistake.

## How a gate that asks us a question can fail to ask

The guard covered one narrow case. Any push to a repo our business partner also reads needed our approval first, and approval meant replying in Discord from our own account. Our agent sessions run with permission prompts turned off, which means a hook has no way to ask us inline. The only channel it had was one we were not looking at, because we were working in our editor.

Five approval requests were created across two sessions. Every one expired unanswered. We told the session to delete the whole thing, because the gate "seems to just be annoying blocks more than help."

The deletion took out the hook, its approval module, their tests and the registration, so the dispatcher went from 15 guards to 14. Earlier that afternoon we had removed a second Discord approval lock, this one on a paid API tool. Its after-the-fact spend alert stayed, since that one tells us and blocks nothing.

Two things stayed on purpose. The check on the autonomous path, which refuses to push to a partner-facing repo unless it is on an allowlist and fails closed, is what stopped an earlier incident in August. That guard was never the one we had been fighting. We also rewrote the messages in three files that still told agents to mint an approval token. Left alone, they would have sent future sessions looking for a module that no longer existed. They now state the rule directly: do not push to a partner-read repo without our go, and ask us in the session.

## Replacing a feeling with a log

Fifteen hours and zero catches is one data point about one guard. When we went to ask the same question of the other fourteen, we found that none of them recorded anything. "We keep getting blocked" could not be checked by us or by anyone else.

So the dispatcher now appends one line to a log file whenever any guard blocks a call. The logging is fail-safe, so a failure to write can never change a guard's decision. We fed it a bare push payload and an unbounded poll loop payload to see that each wrote a correct record.

## The log's first hour was 242 records of test noise

In its first hour the file held 242 records. Almost all of them were written by the test suite. The equivalence tests run the dispatcher as a real subprocess with synthetic payloads, so every guard they exercised wrote a live record. Nine of the records were for a guard that had already been deleted.

That made the log useless for the one question it exists to answer, which is which gates cost us real time. It also broke our standing rule that tests never touch live systems.

The fix is one condition. The block writer does nothing when the test runner's environment variable is set, unless a test explicitly redirects the log somewhere. We archived the 242 polluted records instead of deleting them, so the live log starts empty. Then we checked it in both directions. The 360 tests across the affected files passed and the live log file was never created. A real subprocess run outside the test runner with a bare push payload still exited with the block code and still wrote its record.

The log started clean at 4:21 PM that day. It has nothing to say about the other fourteen guards yet, and the next one that annoys us gets kept or deleted with a count next to it.

## Takeaway

An approval gate that asks on a channel nobody is watching will expire its requests unanswered, so check that the person who must approve can actually see the request. Ask of every guard whether it records its blocks, because without a record "it keeps blocking us" cannot be tested. Keep test traffic out of the log, since a log full of test records cannot answer which gates cost real time.
