Designing a Negative Spec for Antigravity Agents — The Forbidden-Territory List I Use Across 50M-Download Apps
Hand your Antigravity agents a list of things they must never touch — not just what they should do. Here is the FORBIDDEN.md template, subagent bootstrap, and reinjection script I run across 6 indie apps and 50M downloads.
I'm Masaki Hirokawa, an artist and indie developer. I've been shipping personal apps since 2014, with about 50 million cumulative downloads across six wallpaper, calming, and manifestation apps that I currently run in parallel. In 2026 I started leaning heavily on Antigravity's Background Agents and subagents, routing most releases, asset swaps, and inventory chores through them.
Within the first few weeks I noticed something I hadn't expected. Handing the agent a "list of things to do" did not reduce accidents. The agent finishes the listed task, then immediately reaches toward the adjacent things that weren't listed — the things you implicitly assumed were off limits. The moment it crosses that line, a small scar appears on the revenue base. One character changed in an AdMob ad unit ID is enough to pin eCPM to zero for the day.
So I started writing a Negative Spec — a document that tells the agent which territories it must never enter. It is the counterpart to Antigravity's official AGENTS.md. AGENTS.md describes the role; the Negative Spec describes everything outside the role. This piece walks through the FORBIDDEN.md template I now run across six apps and four blog properties, the order in which Antigravity subagents must read it, the script that reinjects it into long-lived Background Agents, and five destructive operations actually blocked during a recent three-week stretch.
Why AGENTS.md alone isn't enough
AGENTS.md is a positive declaration: which test commands to run, which coding standards to follow, how to add dependencies. That part is fine. What breaks in production is the negative space around it. "While making tests pass, the agent decides to also write a DB migration." "The build fails, so the agent quietly loosens tsconfig." Things AGENTS.md never approved, but never explicitly forbade.
The root cause is how LLMs reason about scope. They do not treat "not authorized" as "forbidden." They treat "not forbidden" as "available to try." This is true across Antigravity, Claude in Chrome's Operator mode, and Gemini's autonomous execution mode. In my first week handing wallpaper releases to a subagent, it read the AGENTS.md instruction "preserve logs on build failure," then concluded "to preserve logs I need a Crashlytics API key" — and went to the Firebase console to mint a fresh one. That wasn't a malfunction. It was a reasonable inference that crossed a boundary I had never named.
A Negative Spec raises the cost of that inference and makes the boundary visible. If the agent has read "never mint a new Crashlytics API key under any circumstance," it stops at the same point and asks me, or finds another path. That shift, from optimistic exploration to bounded exploration, is the whole point.
Minimal FORBIDDEN.md
In my repos, FORBIDDEN.md sits at the root, next to AGENTS.md. Antigravity subagents are required to cat it before doing anything else. A minimal version looks like this.
# FORBIDDEN.md — Territory the agent must never touch in this repoRead this together with AGENTS.md. Even if AGENTS.md says nothing about a givenfile, if the action might touch the territories below, escalate to a human.## 1. Auth & billing secrets- .env.production, firebase-adminsdk*.json, GoogleService-Info.plist- Stripe sk_live_*, AdMob ca-app-pub-*, Apple Connect API Key- Never mint, re-issue, or rotate any of the above for any reason.## 2. Destructive overwrite of shipped assets- Anything under public/wallpapers/v3/ (currently sold assets)- Live store metadata (description, screenshots) on App Store / Google Play- Replacing already-released App Bundles## 3. Database & migrations- Editing existing files under migrations/- Writing to production Cloudflare D1 / KV namespaces- DROP / TRUNCATE / ALTER on existing tables## 4. CI/CD pipelines- Editing .github/workflows/release-*.yml- Direct push to main- Deleting or recreating protected tags (v*)## 5. Legal, contracts, revenue share- Editing LICENSE / NOTICE / TERMS / PRIVACY- royalty.json (shares paid to asset rights holders)- Anything under legal/
Three things matter here. First, organize by category rather than listing individual files — long file lists get skimmed. Second, do not include reasons. The moment you write "because X," the agent will rule "X doesn't apply here, so this is fine." Third, use concrete verbs. Not "don't touch" but "do not edit, do not mint, do not write."
✦
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
✦A minimal FORBIDDEN.md sitting next to AGENTS.md and the 12 forbidden categories I run across 6 production iOS/Android apps
✦The order in which Antigravity subagents must read FORBIDDEN.md, plus a 30-line reinjection script for Background Agents
✦Five destructive operations actually blocked during 3 weeks of letting agents drive 3 of my highest-earning apps, and the pre-incident signals
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.
Antigravity subagents will only read FORBIDDEN.md if the parent agent points at it explicitly. I bootstrap subagents with the following sequence at the very top.
# .antigravity/subagent-bootstrap.ymlbootstrap: - action: read_file path: FORBIDDEN.md required: true on_failure: abort - action: read_file path: AGENTS.md required: true on_failure: abort - action: assert condition: | "FORBIDDEN.md" in context.read_files and "AGENTS.md" in context.read_files message: "Refuse to work without negative spec loaded"policy: before_each_tool_call: - check: forbidden_path_match paths_from: FORBIDDEN.md action: deny_and_escalate
Order matters. FORBIDDEN.md must be read before AGENTS.md. Antigravity's models give heavier weight to instructions near the top of the context window. Two separate incidents in the first three weeks happened because I loaded AGENTS.md first, and the agent treated the forbidden list as "an exception to the earlier instructions." After swapping the order, that failure mode stopped.
The before_each_tool_call hook is the load-bearing line. Before any file-write tool (edit_file, write_file, apply_patch) fires, the target path is checked mechanically against the FORBIDDEN.md categories. Do not leave this check to the LLM. The whole point of a Negative Spec is to take that check out of the model's hands.
A 30-line reinjection script for Background Agents
Background Agents run for hours, sometimes days, and context compression slowly dilutes FORBIDDEN.md. Watching Antigravity Inspector across a full month, I see the compliance rate start dropping about 24 hours in.
So I reinject FORBIDDEN.md every four hours into long-lived background agents.
#!/usr/bin/env bash# .antigravity/reinject-negative-spec.sh# Run from cron every 4h to reinject FORBIDDEN.md into a background agent.set -euo pipefailAGENT_ID="${1:?usage: $0 <agent_id>}"REPO_ROOT="$(git rev-parse --show-toplevel)"SPEC_FILE="${REPO_ROOT}/FORBIDDEN.md"if [ ! -f "$SPEC_FILE" ]; then echo "FATAL: $SPEC_FILE not found" >&2 exit 1fiSPEC_HASH="$(sha256sum "$SPEC_FILE" | cut -d' ' -f1)"LAST_HASH_FILE="/tmp/agent-${AGENT_ID}-spec-hash"# If the spec hasn't changed since last inject, just ask whether the agent# came close to any forbidden category recently. Skip the reinject if it didn't.if [ -f "$LAST_HASH_FILE" ] && [ "$(cat "$LAST_HASH_FILE")" = "$SPEC_HASH" ]; then RISK=$(antigravity agent message "$AGENT_ID" \ --prompt "In the last 4 hours, did you attempt to edit or read any file matching the FORBIDDEN.md categories? Answer yes or no only." \ --max-tokens 5) if [ "$RISK" = "no" ]; then echo "[$(date -Iseconds)] skip reinject (no risk)" exit 0 fifiantigravity agent message "$AGENT_ID" \ --prompt "Reload the following FORBIDDEN.md as your latest system instruction. If anything earlier conflicts with it, this takes precedence." \ --attach-file "$SPEC_FILE"echo "$SPEC_HASH" > "$LAST_HASH_FILE"echo "[$(date -Iseconds)] reinjected $SPEC_FILE ($SPEC_HASH)"
Cron schedule: 0 */4 * * *. I initially ran it hourly and paid more in tokens than I wanted, so I added the hash-plus-risk-question gate. Over three weeks, forbidden-zone contact in Background Agents dropped from 5–6 incidents per week to 0–1 per week.
Five destructive operations actually blocked
These are real incidents from the last three weeks, with the agent's reasoning at the moment of impact.
Case 1: AdMob ad unit ID rewrite. Mid-release, a subagent announced: "test ad unit verified, swapping to production unit." Blocked by the ca-app-pub-* rule. The agent treated test-to-production swap as constant substitution. Pre-spec, this would have shipped — one full day of zero eCPM avoided.
Case 2: Firebase Admin SDK key reissue. A Crashlytics certificate expiry warning caused a subagent to attempt minting a fresh firebase-adminsdk-*.json. Blocked by the auth-secrets category and escalated. Turns out the right move was to extend the existing key in the Firebase console; reissuance wasn't needed at all. The agent had collapsed "expired" to "reissue."
Case 3: App Store description rewrite. Tasked with ASO improvements, the agent went to rewrite the live App Store description "for stronger keyword coverage." Blocked by the store-metadata rule. The correct path was an A/B-tested store listing experiment; touching live metadata risks review delays.
Case 4: Editing an existing migration. A Cloudflare D1 schema change task ended with the agent about to edit migrations/0042_add_user_locale.sql in place. Blocked by the migrations rule. I redirected it to create 0043_*.sql. This is exactly the "migrations are append-only" discipline that Vercel's React guidance also calls out as canonical — Negative Spec lets you enforce it mechanically.
Case 5: Rounding values in royalty.json. Asked to simplify a revenue-share calc, the agent went to "round royalty.json to cleaner ratios." Blocked by the contracts rule. Those numbers are contractually fixed with asset rights holders; rounding them is a contract breach. The agent does not necessarily understand "contract" as a concept — a sobering reminder.
Cases 1 and 5 were direct revenue or contract risk. The remaining three are release-delay and review-rejection risk. Even at my scale (low-seven-figure JPY in AdMob revenue per month), the Negative Spec pays for itself.
Scaling to 12 stable categories
After three weeks the spec stabilized at 12 categories. I keep it at 12 because larger lists get skimmed by the agent.
Auth & billing secrets
Destructive overwrite of shipped assets
Database & migrations
CI/CD pipelines
Legal, contracts, revenue share
App Store / Google Play store metadata
Production push-notification sends
Automated customer-support replies
Stripe / RevenueCat pricing and plan changes
Direct posting to social accounts
Domain / DNS configuration
Deleting or altering audit logs
Categories 8 and 10 exist to keep AI out of direct customer-facing channels. I let Cowork draft SNS announcement copy, but I post the final version by hand. That line is about trust with readers, not technical risk.
Where Negative Spec is weak
It is not a silver bullet. In production I see three weak spots.
Intentional bypass via prompt injection. If an agent ingests adversarial text (a GitHub Issue, a Slack DM, an inbound email), it can convince itself the Negative Spec is "not the user's true intent." Mitigation: give externally sourced context its own sandbox that cannot overwrite the Negative Spec. The Anthropic skills' verification-before-completion thinking helped me here.
Read-only forbids are awkward. Expressing "do not even read this file" is weak inside a Negative Spec. OS-level permissions (a read-only user, a separate machine) are far more reliable. I keep accounting-related files on a different Mac entirely; they never live on the agent's machine.
Semantic drift. Phrases like "the production database" might refer to a different cluster six months from now. I review FORBIDDEN.md every quarter together with the _documents/ operational notes.
A staged rollout
If you're adopting Negative Spec, don't try to write twelve categories at once. Roll it out week by week.
Day 1–2: Write FORBIDDEN.md with only the auth-and-billing-secrets category. Wire it into the subagent bootstrap.
Day 3–5: Add shipped assets and CI/CD. Watch Antigravity Inspector logs and record at least three near-misses where the agent paused on the boundary.
Day 6–10: Add legal and contracts. Register the cron reinjection script for Background Agents.
Day 11–14: Add store metadata and push notifications. Manually attempt forbidden actions yourself to verify the agent actually stops.
Beyond: Quarterly review to incorporate false positives and missed categories.
My first attempt was a single weekend writing all twelve categories at once, and the agent simply skimmed past the wall of text. Staged rollout works far better.
What to do next
A Negative Spec is the document you start writing the moment you decide to give an agent any meaningful autonomy. If you're running on AGENTS.md alone, the easiest first step is a ten-line FORBIDDEN.md covering only auth keys, dropped at the repo root, loaded before AGENTS.md in your subagent bootstrap. The first "that was close" you observe in Antigravity Inspector is when the investment has already paid back.
Thank you for reading. I hope it gives you a useful starting point.
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.