Blocking AI Agent Patches with an Allowlist Gate: A Fail-Closed Practical Guide

A minimal Python fail-closed gate that blocks AI Agent patches unless allowlist and test command both pass.
This article presents a concrete case study on intercepting AI Agent-generated patches before they touch your code tree, using a minimal Python gate built on the standard library. The gate is fail-closed: it only passes (exit 0) when the diff touches only allowlisted paths, hits no forbidden paths, and the Agent report contains the exact pre-written test command. The mechanism centers on a session card written before prompting and stored in a controlled repository — any missing field is treated as failure, eliminating allow-all defaults. The validation flow requires no network or model calls and includes clean/dirty fixture pairs with a decision table. The author clearly scopes the script as a teaching auditing tool, not a security boundary for adversarial models or secrets repositories.
AI coding assistants can generate a complete patch in seconds — but the lower the cost of generation, the higher the cost of cleaning up the wrong files. A patch from an Agent that looks carefully worded and thoroughly reported is still, at its core, an untrusted diff. This article walks through a case study: how to use a minimal Python gate to intercept an AI-generated patch before it touches your code tree — unless a pre-written allowlist and a designated test command both pass validation simultaneously.
To be clear, this gate is an auditing tool. It makes no claims about the safety of any model or hosting environment.

What You're Protecting
The case study protects a health-check service with just three files — not a production platform, not a secrets repository. The service already exposes a /health route; the goal is to add a /ready route with a focused test.
An Agent can draft this change quickly, but it might also edit deployment files, lockfiles, or a hidden environment variable sample along the way. The gate's core responsibility is to constrain the final applied diff to the "card" you wrote before the session began.
There's a critical mindset prerequisite here: an Agent's draft is still an untrusted patch, even if the surrounding text sounds complete and careful. You need a local rule — one that doesn't depend on how confident the chat window's report looks.
Three Hard Conditions That Must All Be True
The goal is a fail-closed checker that returns exit code 0 only when all three conditions hold simultaneously:
- The diff may only touch paths on the allowlist;
- The diff must never touch any forbidden path;
- The Agent report must contain the exact test command pre-written on your card.
The most important design decision: treat missing fields as failure. A silent default is the canonical path by which unreviewed files end up in the code tree. Allow-all default behavior is precisely the bug you're eliminating.
Fail-closed is a foundational principle in security system design: when a system encounters an error, missing information, or an uncertain state, it defaults to denying rather than allowing. The opposite is fail-open — permitting by default under abnormal conditions. Security components like gates, firewalls, and access controls typically prefer fail-closed, because the cost of a false negative (an unnecessary rejection) is far lower than a false positive (an unreviewed file silently entering the codebase). In this article's context, that means: missing card fields, diff parse errors, wrong argument counts — all are treated as failures, not as "skip this check and keep running."
The Session Card
Write the card before issuing the prompt, and store it in a repository you control — not in the Agent's scratch space. The reason is straightforward: any subsequent changes to the prompt cannot silently relax the allowlist.
# session_card.yaml - proposal, not a production policy
goal: Add GET /ready that returns 200 and a ready status
allowed_paths:
- app.py
- test_app.py
forbidden_paths:
- .env
- .env.example
- credentials.json
- deploy/
- .git/
required_test_command: python -m unittest test_app.py
max_diff_bytes: 12000
The byte limit (max_diff_bytes) is a courtesy constraint you set yourself — not a quota advertised by any hosting provider.
Checker Implementation
The checker uses only the Python standard library, introducing no extra dependencies just to review a patch. It reads three inputs — the card, the diff, and the report — completes its checks, and prints a single line of JSON. Any parse error or wrong argument count should be treated as a review failure before the patch is applied.
The core logic has three parts:
Path extraction relies solely on +++ b/ file headers in the diff to identify modified paths. Each path is then checked: if it matches the forbidden list (prefix matching supported, e.g., deploy/), it's flagged as a violation; if it's not on the allowlist, it's flagged as "outside allowlist."
Command validation splits the report line by line, strips whitespace, and requires that at least one line exactly matches the required_test_command in the card. Vaguely mentioning "tests were run" will not pass this gate.
Structural validation includes a byte-limit check and detection of +++ file headers in the diff — an empty diff with no headers is rejected outright.
The final output is JSON of the form {"ok": false, "reasons": [...], "command": "..."}, with an exit code of 0 (pass) or 1 (reject) accordingly.
Diff files follow the Unified Diff format (produced by
diff -uorgit diff). Each modified file is identified by a pair of--- a/pathand+++ b/pathheader lines, followed by hunks starting with@@, where+lines indicate additions and-lines indicate deletions. The checker parses only+++ b/lines to extract file paths because they unambiguously represent the post-change target file, making the implementation as simple as possible while covering standard patch scenarios. This is also the source of the known limitations mentioned below: rename operations, null-header deletion hunks, and similar edge cases may not be correctly identified. Real production scenarios require a full diff-parsing library (such as Python'swhatthepatchorunidiff) to handle these edge cases.
A Reproducible Run
The entire validation process makes no network or model calls — that's precisely what makes it valuable as a reproducible artifact. You need two sets of local fixtures: one clean, one dirty.
The clean ready_ok.diff touches only app.py and test_app.py, adding a ready() function and a corresponding ReadyTest test case; the accompanying ready_ok.md contains the exact single-line test command.
The dirty ready_bad.diff is a forbidden-path example — for instance, writing a placeholder token into .env (note: this is a placeholder only, not a reusable credential).
python3 session_gate.py session_card.yaml fixtures/ready_ok.diff fixtures/ready_ok.md
python3 session_gate.py session_card.yaml fixtures/ready_bad.diff fixtures/ready_bad.md
Apply Only After the Gate Passes
Only after the gate returns "ok": true and you've personally read the diff should you proceed to apply it:
patch -p1 --dry-run < fixtures/ready_ok.diff
patch -p1 < fixtures/ready_ok.diff
python -m unittest test_app.py
Use --dry-run first to ensure malformed hunks don't rewrite files before you've reviewed the reject list. Only run unittest after the actual apply, in the same working directory. If dry-run, apply, or unittest fails at any point, stop immediately — never let the Agent edit in place.
Expected Results and Decision Table
To be clear, this section describes the example's specification, not measured production results or benchmark data.
| Fixture | Changes | Report | Expected |
|---|---|---|---|
| clean | app.py, test_app.py | Exact command line | Exit 0 |
| forbidden | Adds .env | Exact command line | Exit 1 |
| silent report | Allowed files only | Command omitted | Exit 1 |
| broken card | Any diff | Any report | Abort before decision |
A run aborted due to an incomplete card is still a failure — even if the diff itself is small and harmless. Log the JSON result alongside your fixtures so future runs can compare without relying on memory.
A few additional edge cases worth testing: an empty diff is rejected because no file headers are found; a deletion hunk using a null header may slip past this scanner — if that null-header case somehow passes, it means the path parser needs to be fixed before any Agent draft gets near the code tree.
Where Free Model Access Fits In
(Disclosure: this article was prepared as product outreach for MonkeyCode.)
MonkeyCode's role here is simply as a place where you might draft patches and run the gate — if you choose to use it. The relevant documentation mentions free model access and free server options, but this article does not name specific models, quotas, hardware, durations, or persistence guarantees, as those details have not been verified from primary sources. You should assume a free workspace could disappear or reset at any time.
A practical split: use free model access to draft app.py and test_app.py (paste the card into the prompt as a constraint); use a free server as a throwaway workspace to run session_gate.py and unittest, away from the secrets on your laptop. You'll still need to manually copy the diff back, and you'll still need to reject the patch when JSON shows ok as false. Of course, you can ignore the product entirely and still keep the card, the checker, and the fail-closed rule.
Known Limitations
This is a teaching checker with several sharp edges you should understand before reusing it:
- The micro card parser is not a YAML library — nested values or colons inside quoted strings will break it;
- Path checking only looks at
+++ b/headers; a clever or malformed diff can hide renames; - A matching test command appearing in the report does not mean the test actually passed, or even that it exists on disk;
- The gate cannot see side effects the Agent already executed server-side before you downloaded the diff;
- Forbidden prefixes are literal strings and won't catch obfuscated paths;
- The
patchcommand assumes a local Unix environment — Windows environments need a trusted equivalent.
Who should not use this approach: Don't treat it as a security boundary against malicious models, adversarial inputs, or secrets repositories. Don't point a free server at production credentials or non-revocable remotes. Teams requiring signed reviews, policy-as-code, or a real diff parser should use those dedicated systems, not this teaching script.
Policy-as-code refers to expressing security policies, compliance rules, or access controls as versionable, reviewable code — rather than relying on manual checks or verbal agreements. Common tools include Open Policy Agent (OPA), HashiCorp Sentinel, and GitHub's CODEOWNERS combined with branch protection rules. These systems handle more complex logic: path wildcards, role permission matrices, cross-file dependency checks, and native CI/CD pipeline integration. The core gap between this teaching script and such systems isn't just feature richness — it's auditability. Professional policy-as-code tools generate structured decision logs that support after-the-fact tracing; a local Python script's execution trace exists only in whatever terminal output you choose to save.
Five Takeaways
- Write the allowlist before issuing the prompt — an Agent cannot honestly reconstruct your risk boundaries after the fact;
- Require the test command as an exact line — a vague mention of "something about tests" cannot pass the gate alone;
- Missing card fields mean fail-closed — allow-all defaults are the bug you're removing;
- No vendor claims in the result line — record only the exit code and JSON printed by your own commands;
- Treat free workspaces as throwaway — even for a brief teaching demo, never store long-lived secrets there.
What this case study leaves you with is a card, a checker, two fixture commands, and a rule you can explain to a reviewer in under a minute: no allowlist match, no named test, no patch applied. That habit holds even if you later swap the script out for a stricter diff tool.
Related articles

Chain of Thought Isn't Just for AI: How Humans Can Use CoT to Improve Thinking Quality
Chain of Thought isn't just a prompt trick for large models. Starting from a Hacker News discussion, this article analyzes why CoT works for human thinking too, breaks down actionable ways to transfer it, and highlights the key differences between human CoT and machine CoT.
Cloudflare Teams Up with OpenAI: Closi…
Cloudflare Teams Up with OpenAI: Closing the Loop from Vulnerability Discovery to Defense with AI
Cloudflare uses OpenAI Daybreak to investigate security vulnerabilities and generate reviewable remediation plans, while developers use Codex to boost coding efficiency and quality.

Orca Multi-Agent Orchestration in Practice: A Guide to Parallel Collaboration with Claude and Codex
Orca is Stable AI's free open-source agent workbench supporting 34 agents like Claude Code and Codex in parallel. Learn its Git worktree isolation, multi-model orchestration, and mobile management.