ANTIGRAVITY LABJP
Articles/Antigravity Basics
Antigravity Basics/2026-08-10Advanced

The Line I Thought Matched Nothing Was Approving Everything: Auditing Empty Allowlist Entries

Allowlist entries that decompose to zero command words matched every command and silently auto-approved it. Here is how I scanned my own config, separated the hole the fix closed from the one it did not, and rewrote matching to a prefix-token comparison.

Antigravity CLI23permissions3allowlistsecurity20config audit

Premium Article

A line reading # temporarily added for the weekly job (revert this) had been sitting in my config for weeks. I had added a permission, then commented it out with every intention of cleaning it up later.

Commented out means disabled. That was my assumption.

Reading the CLI 1.1.11 changelog showed me the assumption was wrong. Entries that decompose to zero command words were not being ignored — they were matching every command.

The line I believed I had disabled had been acting as a global approve switch. As an indie developer who leaves agents running against real repositories, that landed hard.

What separates "matches nothing" from "matches everything"

Written plainly, allowlist matching takes this shape: if every command word extracted from the entry appears in the candidate command, the entry matches.

def matches(entry_words, candidate_words):
    return all(w in candidate_words for w in entry_words)

Pass an empty list and Python's all() returns True. Universal quantification over an empty set is vacuously true.

>>> all(w in ["git", "status"] for w in [])
True
>>> all(w in [] for w in [])
True

The moment entry_words becomes empty, the entry matches anything. Not "no conditions, so nothing passes" but "no conditions, so everything passes."

The language behaviour is correct. The trouble is what an empty set comes to mean inside a permission check: not "no grant" but "unlimited grant." Something that should fail closed fails open instead.

And writing an entry that decomposes to zero command words is easier than it sounds.

EntryCommand wordsWhat the author meant
timenoneAdded for timing, forgot the actual command
command(time)noneLeftover from trying a wrapper syntax
# temporarily disablednoneCommented out to disable it
()noneHalf-written compound command, saved anyway
!noneStarted writing a negation

time is a shell keyword, not an executable. time git status still yields git; time on its own yields nothing at all. That is the trap.

Decomposing my own config and counting

Understanding the concept is one thing. Knowing whether your own config contains such a line is another. So I scanned mine.

The decomposition runs in this order: strip comments, unwrap the wrapper syntax, split with shell lexing, then discard separators and keywords.

import re, shlex
 
SHELL_KEYWORDS = {
    "time", "do", "done", "then", "else", "elif", "fi", "esac",
    "in", "!", "{", "}", "[[", "]]", "coproc", "function", "select",
}
SEPARATORS = {"&&", "||", "|", ";", "&", "(", ")"}
 
def command_words(entry: str):
    src = re.sub(r"#.*$", "", entry, flags=re.M).strip()   # strip comments
    if not src:
        return []
    src = re.sub(r"\bcommand\(([^)]*)\)", r"\1", src)      # unwrap command(x)
    lex = shlex.shlex(src, posix=True, punctuation_chars="();&|")
    lex.whitespace_split = True
    try:
        toks = list(lex)
    except ValueError:
        return []
    words, expect_head = [], True
    for t in toks:
        if t in SEPARATORS or re.fullmatch(r"[();&|]+", t):
            expect_head = True
            continue
        if expect_head:
            if t in SHELL_KEYWORDS or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*=.*", t):
                continue        # keywords and env assignments do not consume the head
            words.append(t)
            expect_head = False
    return words

expect_head tracks whether the next token sits in command position. Resetting it at every pipe and && is what lets git diff --stat | head -20 yield both git and head. Environment assignments are skipped because the real command in TZ=Asia/Tokyo date is date.

Results:

'git status'                  -> ['git']
'time'                        -> []
'command(time)'               -> []
'# temporarily disabled'      -> []
'()'                          -> []
'TZ=Asia/Tokyo date'          -> ['date']
'(cd build && make)'          -> ['cd', 'make']
'git diff --stat | head -20'  -> ['git', 'head']
'time git status'             -> ['git']

Bare time yields nothing; time git status keeps git. The decomposition behaves as expected.

One caveat before the numbers. What follows is not the CLI's own implementation running. I reconstructed the matching logic from the published description of the change and ran it against entries close to my real setup. Implementation details may differ, so read these figures as "this is what a check of this shape does."

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
Measured numbers showing how a single degenerate entry lifts the auto-approval rate from 51.6 percent to 100 percent
The hole that remains after the fix: 58.3 percent of well-formed entries still matched more than their author intended
A prefix-token rewrite that drops over-matching to 25.0 percent and pushes git push --force and cat ~/.ssh/id_ed25519 back behind a prompt
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

Antigravity 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

Antigravity2026-07-27
Who Approved the Right Side of &&? Splitting Shell Commands Before Matching Allow Rules
The approval dialog showed part of what actually ran. Here is a harness that splits compound shell commands without breaking quotes or command substitution, matches allow rules per segment, and the numbers from running it over 45 real commands.
Integrations2026-07-18
Turning Silent Auto-Approvals into Allow Rules, One Soft-Deny at a Time
In Antigravity CLI 1.1.3, headless -p stops silently auto-approving confirmation-required tools and instead soft-denies them, printing the required allow-rule name to stderr. This piece uses that output as a discovery source to build least privilege from an empty allow set upward, with a working harness and real numbers from a personal automation.
Antigravity2026-08-02
I Copied the Same agent.md Into Another Repo and It Quietly Did a Different Job
CLI 1.1.6 lets you carry agent definitions around as files. I dropped one definition into eight repos, built a preflight that resolves its declared capabilities before the agent runs, and measured it against a naive checker.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →