◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 8 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 8 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/Dev Tools
⟐ Dev Tools/2026-09-15Advanced

The approval rules I had written never matched once — auditing my Gemini CLI policies

A deny rule I trusted for half a year had never matched. The pattern was tested against a JSON string, the folder I kept my rules in is not read, and ask_user turns into deny the moment nobody is watching. Here is the audit, and the script I wrote to keep doing it.

gemini-cli7policy-engineapproval-modemcp3automation54python106

✦ Premium Article

The asset conversion I had left running overnight had not moved a single line by morning.

As an indie developer I keep a set of wallpaper apps, and the dull part of shipping new images — resizing to fixed dimensions, renaming to match a convention — is something I hand to a coding agent. The work itself is simple enough that I only wanted to write down where the line sits, then leave it alone.

I opened the log expecting to find why it had stalled. What I found instead was less comfortable. The rule that was supposed to stop the dangerous command had never matched anything at all. Something else had stalled the run, and the line I had trusted for half a year was swinging at air.

I spent that day reading my policy files from the top. Here is what came out.

The short version: I recounted where matching actually happens

A policy rule is not a string compared against your command. The thing it is compared to, the folder it is loaded from, and the mode you are running in all change the answer. The four problems I hit were not about writing the rule wrong. In every case what I wrote never reached the point of being evaluated.

SymptomWhat was really happeningHow it shows up
A deny rule does nothingThe regex anchored to the start of a JSON stringYou never once see the deny message
Project-level rules are ignoredThat tier is not loaded right nowMoving the file to the user tier fixes it instantly
Unattended runs refuse instead of waitingask_user is treated as denyThe same run passes interactively
No confirmation on redirectionThe downgrade is skipped in permissive modesDefault mode does prompt

Let me take them one at a time.

A ^ in commandRegex anchors to the JSON, not to the command

This was the line I had written first, meaning to stop destructive deletes.

[[rule]]
toolName = "run_shell_command"
commandRegex = "^rm -rf"
decision = "deny"
priority = 900

There is nothing wrong with it as a regular expression. But the reference says commandRegex is tested against a stable JSON representation of the arguments. So the subject is not the command — it is the single string {"command":"rm -rf ./build"}.

I checked on my own machine.

import json
import re
 
args = {"command": "rm -rf ./build"}
subject = json.dumps(args, separators=(",", ":"))
print("subject:", subject)
 
for pattern in ("^rm -rf", "rm -rf", '"command":"rm -rf'):
    print(f"  {pattern!r:<22} -> {bool(re.search(pattern, subject))}")
subject: {"command":"rm -rf ./build"}
  '^rm -rf'              -> False
  'rm -rf'               -> True
  '"command":"rm -rf'    -> True

The moment you add the anchor, the rule stops matching. Worse, nothing tells you. A deny rule that swings at air prints exactly the same thing as one that never had to fire — nothing. A deny rule you have never watched actually deny something is a rule with no evidence behind it.

If you want anchoring, commandPrefix is the safer tool: it means what it looks like it means. Keep commandRegex for conditions that genuinely need a regex, and leave the anchors out. That alone cleared up my swings.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to find, mechanically and from your own files, the approval rules that have never matched a single call
✦You will know which tier actually gets read, so you stop writing careful rules into a folder nobody loads
✦You will be able to decide where ask_user belongs and where it has to become an explicit allow or deny, before an unattended run quietly refuses itself
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Dev Tools2026-10-14
Stop Re-Reading Every Crash Report: Grep a Known-Bug Table First and Keep Only an Index in GEMINI.md
A routine for investigating app crash reports without making the model read every past bug in full: a one-line-per-bug lookup table, what belongs in GEMINI.md, and a grep helper you can paste in.
⟐ Dev Tools2026-09-29
Gemini CLI Kept Saying 'API key not valid' After I Gave It a New Key — It Was Reading an Old One From a Third Place
I switched Gemini CLI from Google login to a paid API key, and the 400 'API key not valid' error would not go away. The key can come from four places — the shell, a project .env, a home .env, and settings.json — and a short script that lights them up in order narrows the cause to one.
⟐ Dev Tools2026-05-02
Google ADK Quickstart — Build Your First AI Agent in 30 Minutes
A hands-on Google Agent Development Kit (ADK) walkthrough for absolute beginners — from install to a working agent in 30 minutes, with the gotchas called out as you hit them.