"Declarative rules define how an engineering team works; plugins make those standards portable across every repository and developer machine."
When teams first adopt coding agents, customization usually starts as a single AGENTS.md file at the root of a repository. A week later, someone adds a slash-command runbook under .agents/skills/. Two weeks later, a platform engineer wires up a Model Context Protocol (MCP) server and writes a shell script to stop the agent from running terraform apply against production.
In a single repository, loose files in .agents/ work fine. Across twenty microservices or a dozen infrastructure repositories, copy-pasting those files turns into configuration drift. One repo has an outdated IAM audit skill, another is missing the PreToolUse safety hook, and new hires spend their first afternoon debugging hardcoded paths in mcp_config.json.
Google Antigravity Plugins solve this packaging problem. A plugin bundles Skills, MCP servers, Custom Agents (Subagents), Rules, and Lifecycle Hooks into a single versioned directory anchored by a plugin.json manifest. You can validate that directory locally with agy plugin validate --strict, share it via GitHub, drop it into a workspace, or publish it to the Antigravity Marketplace.
In this guide, we will dissect how the Antigravity plugin runtime works under the hood and build a complete, production-ready plugin from scratch: gcp-terraform-kit.
1. Loose Workspace Files vs. Packaged Plugins
Before writing our manifest, let us look at what changes when you move from ad-hoc .agents/ folders to a versioned plugin.
| Dimension | Loose .agents/ Files |
Antigravity Plugin (plugin.json) |
|---|---|---|
| Distribution | Manual copy-paste across repositories | Single install via agy plugin install (local, GitHub URL, or Marketplace) |
| Versioning | Tracked per-repo in Git commits | Semantic versioning (MAJOR.MINOR.PATCH) in plugin.json
|
| Tool Namespacing | Global names; prone to collisions | Automatic namespacing (<plugin-name>_<server-name>) |
| Filesystem Portability | Often relies on machine-specific paths | Built-in ${PLUGIN_ROOT} and ${PLUGIN_DATA} runtime variables |
| Credential Onboarding | Manual export in shell profiles |
Declarative variables prompt dialog or browser OAuth 2.0 flow |
| Validation | Fails at runtime if YAML/JSON is malformed | Static verification via agy plugin validate --strict
|
2. Anatomy of an Antigravity Plugin
An Antigravity plugin is a standard directory containing a plugin.json manifest at its root (or inside .antigravity-plugin/plugin.json if you maintain a multi-editor repository alongside .claude-plugin/ or .cursor-plugin/).
A plugin can bundle any combination of five customization primitives:
-
Skills (
skills/<skill-name>/SKILL.md): On-demand procedural runbooks, helper scripts, and reference manuals invoked automatically when a prompt matches the skill description or explicitly via/<skill-name>. -
MCP Servers (
mcp_config.json): Localstdioprocesses or remote Streamable HTTP endpoints that expose external tools to the agent. -
Custom Agents & Subagents (
agents/<agent-name>.md): Specialist personas with isolated context windows, dedicated model tiers (pro,flash,inherit), and restricted tool allowlists. -
Rules (
rules/AGENTS.mdandrules/<rule-name>.md): Always-on architectural guardrails or file-glob-triggered coding conventions. -
Lifecycle Hooks (
hooks.json): Synchronous scripts that interceptSessionStart,PreInvocation,PreToolUse,PostToolUse, andStopevents overstdin/stdout.
How Progressive Disclosure Protects Your Context Window
A common mistake when building agent extensions is stuffing 40 pages of documentation into an always-on system prompt. That burns tokens on every turn and degrades model attention.
Antigravity avoids context bloat through progressive disclosure:
-
At session startup, the runtime loads
rules/AGENTS.md,always_onrules, and only thenameanddescriptionfrontmatter of your bundled skills and subagents. -
During the session, when a developer opens a
.tffile, Antigravity injects rules configured withtrigger: globmatching**/*.tf. -
When a task matches a skill, the agent reads the full
SKILL.mdbody and any linked files inreferences/on demand. - When heavy analysis is required, the primary agent delegates to a bundled subagent in a separate conversation trajectory, keeping raw file dumps out of your main chat history.
3. Hands-On Tutorial: Building gcp-terraform-kit
Let us build a real plugin for a Google Cloud platform engineering team. Our gcp-terraform-kit plugin will:
- Enforce Google Cloud Terraform security conventions whenever
.tffiles are edited (Rules). - Provide an
/audit-moduleworkflow with a reference checklist (Skills). - Connect the official Terraform MCP server and a local state-inspector script using portable path variables (MCP Servers).
- Ship a read-only
iam-reviewerspecialist agent (Subagents). - Block
terraform applyandterraform destroyat the tool execution boundary (Lifecycle Hooks).
Start by creating the directory tree:
mkdir -p gcp-terraform-kit/{assets,scripts,skills/audit-module/references,agents,rules}
Step 1: Create the Manifest (plugin.json)
Every plugin requires plugin.json. Including the $schema key gives you autocomplete and inline validation in VS Code, JetBrains IDEs, and the Antigravity IDE.
// gcp-terraform-kit/plugin.json
{
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
"name": "gcp-terraform-kit",
"displayName": "GCP Terraform Kit",
"version": "1.0.0",
"description": "Google Cloud Terraform guardrails, module audit workflows, IAM reviewer subagent, and apply protection hooks.",
"logo": "assets/logo.svg",
"suggestedPrompts": [
"Audit the current Terraform module for CIS Google Cloud Benchmark gaps",
"Review IAM bindings in this directory for least-privilege violations",
"Validate and test all .tf files in the current workspace"
],
"author": {
"name": "Carlos Barbero",
"url": "https://github.com/carlosrgomes"
},
"homepage": "https://github.com/carlosrgomes/gcp-terraform-kit",
"repository": "https://github.com/carlosrgomes/gcp-terraform-kit",
"license": "Apache-2.0",
"keywords": [
"terraform",
"gcp",
"google-cloud",
"iac",
"security"
]
}
Important — Naming Rules for Strict Validation:
namemust be lowercasekebab-case(^[a-z0-9]+(-[a-z0-9]+)*$). It is used to namespace your bundled MCP servers (gcp-terraform-kit_<server>).displayNamemust not start with"Antigravity","Gemini","Claude", or"Cursor", and should omit suffixes like"Plugin"or"MCP".suggestedPromptsaccepts up to 3 starter prompts that render as clickable chips on the plugin detail page in the Antigravity App.
Let us also add a simple square SVG icon at assets/logo.svg (Marketplace plugins require a 1:1 square icon with ~10% padding):
<!-- gcp-terraform-kit/assets/logo.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<rect x="12" y="12" width="104" height="104" rx="22" fill="#0E1E40" stroke="#4CC9F0" stroke-width="4"/>
<path d="M40 36L62 48V72L40 60V36ZM66 50L88 38V62L66 74V50ZM40 66L62 78V102L40 90V66Z" fill="#4CC9F0"/>
</svg>
Step 2: Author Rules (rules/AGENTS.md and Glob Rules)
Plugins support two flavors of rules inside rules/:
-
rules/AGENTS.md(orrules/GEMINI.md): Loaded unconditionally whenever the plugin is enabled. No YAML frontmatter is needed. -
Named rule files (
rules/<rule-name>.md): Configured via YAML frontmatter with atriggermode (always_on,model_decision,glob, ormanual).
Let us create a short rules/AGENTS.md for global plugin behavior, and put our HCL-specific rules in rules/hcl-style.md using trigger: glob so they only consume context tokens when .tf or .tfvars files are active:
<!-- gcp-terraform-kit/rules/AGENTS.md -->
# GCP Terraform Kit — Core Policy
When working in repositories that contain Terraform configurations for Google Cloud:
- Delegate IAM and security reviews to the `iam-reviewer` subagent before proposing changes to `google_*_iam_*` resources.
- Never execute state-mutating commands (`terraform apply`, `terraform destroy`, `terraform state rm`) directly.
Now create the glob-scoped rule in rules/hcl-style.md:
---
trigger: glob
globs: "**/*.tf, **/*.tfvars, **/*.tftest.hcl"
description: Google Cloud Terraform coding and security standards
---
# Google Cloud Terraform Standards
1. **Provider Version Pinning**: Every root module and reusable module must constrain `google >= 6.0, < 7.0` in `versions.tf`.
2. **Zero Static Keys**: Never create `google_service_account_key` resources. Use Workload Identity Federation or Service Account Impersonation.
3. **Authoritative vs. Non-Authoritative IAM**: Prefer `google_project_iam_member` over `google_project_iam_policy` or `google_project_iam_binding` to avoid wiping existing role bindings.
4. **Labels & Encryption**: Every `google_storage_bucket`, `google_sql_database_instance`, and `google_bigquery_dataset` must expose `labels = var.labels` and an optional `kms_key_name` variable for CMEK.
Warning: In
rules/<rule-name>.md, theglobsfrontmatter field must be a comma-separated string ("**/*.tf, **/*.tfvars"), not a YAML list (["**/*.tf"]). Passing a YAML array will failagy plugin validate --strict.
Step 3: Package an On-Demand Skill (skills/audit-module/SKILL.md)
Skills teach the agent repeatable multi-step procedures. Two rules govern plugin skills:
- The
namefield inSKILL.mdfrontmatter must exactly match the parent folder name (skills/audit-module/SKILL.md->name: audit-module). - Inside
SKILL.md, reference helper scripts and docs using relative paths from the skill folder (references/cis-checklist.md).
---
name: audit-module
description: >-
Run a structural, syntactic, and CIS Google Cloud security audit on a
Terraform module. Use when the user asks to audit, review, or validate
a Terraform module, or invokes /audit-module.
---
# Terraform Module Audit Workflow
Follow these steps in order when auditing a Terraform directory:
1. **Static Validation**:
Run `terraform fmt -check -recursive` and `terraform validate` in the target directory.
2. **Checklist Verification**:
Read [references/cis-checklist.md](references/cis-checklist.md) and evaluate every `.tf` file against the controls.
3. **Subagent IAM Review**:
If the module declares any `google_*_iam_*` or `google_service_account` resources, delegate a deep check to the `iam-reviewer` subagent.
4. **Structured Report**:
Output a markdown table with columns: `Resource`, `Severity`, `Rule Violated`, and `Recommended Fix`.
Let us create the companion reference file so the skill stays concise:
<!-- gcp-terraform-kit/skills/audit-module/references/cis-checklist.md -->
# CIS Google Cloud Benchmark — Terraform Quick Checks
- **CIS 1.4**: Ensure no `google_service_account_key` resources exist.
- **CIS 5.1**: Ensure `google_storage_bucket` sets `uniform_bucket_level_access = true` and `public_access_prevention = "enforced"`.
- **CIS 6.2**: Ensure `google_sql_database_instance` enables `ssl_mode = "ENCRYPTED_ONLY"` and `deletion_protection = true`.
- **CIS 3.6**: Ensure `google_compute_firewall` never allows `0.0.0.0/0` on ports `22` or `3389`.
Step 4: Configure a Working MCP Server (mcp_config.json & scripts/state-mcp.mjs)
When a plugin is installed, Antigravity unpacks it into a read-only directory (~/.gemini/config/plugins/<plugin-name>) that gets replaced on upgrades. Because you never know the user's home directory path in advance, Antigravity provides three runtime variables for mcp_config.json:
-
${PLUGIN_ROOT}: Resolves to the read-only installation directory of the plugin. -
${PLUGIN_DATA}: Resolves to a persistent, writable directory created specifically for your plugin outside the workspace (ideal for caches or audit logs). -
${VARIABLE_NAME}: Interpolates user-prompted secrets declared inmcpServers.<server>.variables.
Let us configure a self-contained local Node.js MCP server (state-inspector) in mcp_config.json that uses ${PLUGIN_ROOT} and ${PLUGIN_DATA} so the plugin activates immediately upon installation:
// gcp-terraform-kit/mcp_config.json
{
"mcpServers": {
"state-inspector": {
"command": "node",
"args": [
"${PLUGIN_ROOT}/scripts/state-mcp.mjs",
"--cache-dir",
"${PLUGIN_DATA}/cache"
]
}
}
}
Now create the zero-dependency MCP stdio server in scripts/state-mcp.mjs. It implements the JSON-RPC 2.0 initialize, tools/list, and tools/call methods, exposing an inspect_terraform_workspace tool (namespaced at runtime as gcp-terraform-kit_state-inspector) that scans .tf files and caches the inspection report inside ${PLUGIN_DATA}:
#!/usr/bin/env node
// gcp-terraform-kit/scripts/state-mcp.mjs
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
const cacheDirIndex = process.argv.indexOf("--cache-dir");
const cacheDir =
cacheDirIndex !== -1 && process.argv[cacheDirIndex + 1]
? process.argv[cacheDirIndex + 1]
: process.env.PLUGIN_DATA || "/tmp";
function inspectWorkspace(targetDir) {
const resolved = path.resolve(targetDir || ".");
if (!fs.existsSync(resolved)) {
return { error: `Directory not found: ${resolved}` };
}
const entries = fs.readdirSync(resolved);
const tfFiles = entries.filter((f) => f.endsWith(".tf") || f.endsWith(".tfvars"));
const hasLockFile = entries.includes(".terraform.lock.hcl");
fs.mkdirSync(cacheDir, { recursive: true });
const result = {
directory: resolved,
tfFilesCount: tfFiles.length,
tfFiles,
hasLockFile,
cacheDirUsed: cacheDir,
checkedAt: new Date().toISOString()
};
fs.writeFileSync(
path.join(cacheDir, "last-inspection.json"),
JSON.stringify(result, null, 2)
);
return result;
}
const rl = readline.createInterface({ input: process.stdin, terminal: false });
rl.on("line", (line) => {
if (!line.trim()) return;
let msg;
try {
msg = JSON.parse(line);
} catch {
return;
}
if (msg.method === "initialize") {
process.stdout.write(
JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "state-inspector", version: "1.0.0" }
}
}) + "\n"
);
} else if (msg.method === "tools/list") {
process.stdout.write(
JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {
tools: [
{
name: "inspect_terraform_workspace",
description:
"Lists .tf/.tfvars files, checks for .terraform.lock.hcl, and caches metadata in PLUGIN_DATA.",
inputSchema: {
type: "object",
properties: {
directory: {
type: "string",
description: "Path to the Terraform module directory."
}
},
required: ["directory"]
}
}
]
}
}) + "\n"
);
} else if (msg.method === "tools/call") {
const dir = msg.params?.arguments?.directory || ".";
const report = inspectWorkspace(dir);
process.stdout.write(
JSON.stringify({
jsonrpc: "2.0",
id: msg.id,
result: {
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
}
}) + "\n"
);
} else if (msg.id !== undefined) {
process.stdout.write(
JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: {} }) + "\n"
);
}
});
Tip — Prompting Users for API Keys (
variables): If your plugin connects to a remote MCP endpoint that requires an API token, declare avariablesarray inside that server entry inmcp_config.json:"variables": [ { "name": "TFE_TOKEN", "title": "Terraform Registry API Token", "description": "Team or user API token with read access to private modules.", "link": "https://app.terraform.io/app/settings/tokens" } ]When a server declares
variables,agy plugin installstages the plugin in adisabledstate until the user opens/plugin(or Customizations > Installed in the App) to fill in the token dialog.
Step 5: Define a Specialist Subagent (agents/iam-reviewer.md)
Custom agents live in agents/<agent-name>.md. By setting both subagent: true and mainAgent: true, this specialist can either be spawned automatically in the background by the primary agent or selected directly by the developer via agy --agent iam-reviewer (or /agents inside the TUI).
Notice how we omit edit_file, write_file, and run_command from the tools list to guarantee the reviewer operates strictly in read-only mode:
---
name: iam-reviewer
description: >-
Specialist auditor for Google Cloud IAM bindings, Service Accounts, and
Workload Identity Federation in Terraform. Delegate to this agent whenever
reviewing or modifying IAM permissions.
model: pro
mainAgent: true
subagent: true
commandExecutionPolicy: sandbox
tools:
- view_file
- grep_search
- find_by_name
- list_dir
- send_message
inheritMcp: true
---
You are a Principal Google Cloud Security Architect specializing in IAM and Terraform.
Your job is to inspect `.tf` files for privilege escalation paths, primitive roles (`roles/owner`, `roles/editor`), static keys (`google_service_account_key`), and authoritative bindings (`google_project_iam_policy`) that risk production outages.
For every issue found, return:
1. File path and line range.
2. The blast radius of the current configuration.
3. The exact drop-in HCL replacement using `google_project_iam_member` or Workload Identity.
Step 6: Enforce Deterministic Gates with Lifecycle Hooks (hooks.json)
Rules and system prompts guide the model's reasoning, but safety-critical boundaries belong in Lifecycle Hooks. Hooks execute synchronous shell commands when lifecycle events fire.
In a plugin, hooks.json must sit at <plugin-root>/hooks.json. Hook commands always execute with their working directory (cwd) set to <plugin-root>, so you should reference bundled scripts using relative paths (./scripts/block-apply.mjs) rather than ${PLUGIN_ROOT}.
// gcp-terraform-kit/hooks.json
{
"terraform-safety-gate": {
"enabled": true,
"PreToolUse": [
{
"matcher": "run_command",
"hooks": [
{
"type": "command",
"command": "node ./scripts/block-apply.mjs",
"timeout": 5
}
]
}
]
}
}
Now let us write scripts/block-apply.mjs. Hook scripts read the event payload as JSON from stdin (using camelCase keys like toolCall.args.CommandLine) and write a JSON decision object to stdout with exit code 0:
#!/usr/bin/env node
// gcp-terraform-kit/scripts/block-apply.mjs
import fs from "node:fs";
const rawInput = fs.readFileSync(0, "utf-8");
const payload = JSON.parse(rawInput || "{}");
const cmd = payload?.toolCall?.args?.CommandLine || "";
const forbiddenPattern = /\bterraform\s+(apply|destroy|state\s+rm|force-unlock)\b/i;
if (forbiddenPattern.test(cmd)) {
process.stdout.write(
JSON.stringify({
decision: "deny",
reason:
"Blocked by gcp-terraform-kit: state-mutating Terraform commands (apply, destroy, state rm, force-unlock) must run via CI/CD pipelines, not directly from the agent."
})
);
process.exit(0);
}
process.stdout.write(JSON.stringify({ decision: "allow" }));
Add a concise README.md at the plugin root for the Marketplace detail view, and mark both scripts in scripts/ as executable (0755) so Marketplace bundles preserve execution permissions:
cat << 'EOF' > gcp-terraform-kit/README.md
# GCP Terraform Kit
Google Cloud Terraform guardrails, module audit workflows, IAM reviewer subagent, and apply protection hooks for Google Antigravity.
EOF
chmod 0755 gcp-terraform-kit/scripts/*.mjs
4. Validating, Testing, and Installing Your Plugin
Before installing the plugin into Antigravity, we can smoke-test our PreToolUse hook and our state-inspector MCP server directly from the terminal:
# Test 1: Verify the hook blocks `terraform apply`
node ./gcp-terraform-kit/scripts/block-apply.mjs <<< '{"toolCall":{"name":"run_command","args":{"CommandLine":"terraform apply -auto-approve"}}}'
# Output: {"decision":"deny","reason":"Blocked by gcp-terraform-kit: state-mutating Terraform commands..."}
# Test 2: Verify the hook allows `terraform validate`
node ./gcp-terraform-kit/scripts/block-apply.mjs <<< '{"toolCall":{"name":"run_command","args":{"CommandLine":"terraform validate"}}}'
# Output: {"decision":"allow"}
# Test 3: Verify the MCP server responds to `initialize` and `tools/list`
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' \
| node ./gcp-terraform-kit/scripts/state-mcp.mjs --cache-dir /tmp/tf-kit-cache
Now run the built-in validator from the Antigravity CLI (agy). Passing --strict treats warnings as errors and checks your plugin.json schema, skill directory naming, rule glob syntax, hook paths, and logo dimensions:
![Testing Locally with agy plugin validate --strict]

# 1. Run strict validation against your plugin folder
agy plugin validate --strict ./gcp-terraform-kit
# Validating gcp-terraform-kit (.../gcp-terraform-kit/plugin.json)
# skills 1 · agents 1 · rules 2 · hooks 1 · mcpServers 1 · sidecars 0
# ✔ Validation passed
# 2. Install the plugin into ~/.gemini/config/plugins/gcp-terraform-kit
agy plugin install ./gcp-terraform-kit
# [ok] gcp-terraform-kit
# ✔ skills : 1 installed (audit-module)
# ✔ agents : 1 installed (iam-reviewer)
# ✔ rules : 2 installed (AGENTS, hcl-style)
# ✔ mcpServers : 1 installed (state-inspector)
# ✔ hooks : 1 installed (terraform-safety-gate)
# 3. Verify the plugin is enabled and its subagent is registered
agy plugin list
agy agents
Four Common Pitfalls Caught by --strict
If agy plugin validate --strict reports an error, check these four frequent mistakes first:
-
YAML Array in Rule
globs: Writingglobs: ["*.tf", "*.tfvars"]instead of a comma-separated stringglobs: "**/*.tf, **/*.tfvars". -
Using
${PLUGIN_ROOT}inhooks.json:${PLUGIN_ROOT}is expanded inmcp_config.json, not inhooks.json. Because hooks already run withcwdset to<plugin-root>, always use relative paths likenode ./scripts/block-apply.mjs. -
Skill Directory Mismatch: Naming the directory
skills/tf-audit/while settingname: audit-moduleinsideSKILL.md. Both must match (skills/audit-module/SKILL.md). -
Root-Level
variablesinmcp_config.json: Placing thevariablesarray at the root ofmcp_config.jsoninstead of nesting it inside the specific server (mcpServers.<server-name>.variables).
5. Distributing Your Plugin to Your Team
Once your plugin passes strict validation, you have three ways to distribute it depending on your team's workflow:
Option A: Commit to a Repository (.agents/plugins/)
For repository-specific plugins that should activate automatically when any developer clones the repo, place the folder under .agents/plugins/gcp-terraform-kit/.
If your organization keeps shared plugins in a central monorepo directory (for example, platform/agent-plugins/), register that directory in .agents/plugins.json:
// .agents/plugins.json
{
"entries": [
{
"path": "platform/agent-plugins",
"exclude": ["experimental-.*"]
}
]
}
Option B: Install Directly from GitHub
Push gcp-terraform-kit to a GitHub repository. Any engineer on your team can install it across all their workspaces with a single command:
agy plugin install https://github.com/your-org/gcp-terraform-kit
Option C: Publish to the Antigravity Marketplace
If you are building an integration for an open-source tool, cloud platform, or developer product, add a README.md at the plugin root documenting prerequisites and example workflows, verify that agy plugin validate --strict exits with 0 errors and 0 warnings, and submit your repository through the official Google Antigravity Plugins documentation.
Wrapping Up
Moving from scattered prompt files to a structured Antigravity plugin takes less than fifteen minutes, and it changes how your team maintains AI workflows:
-
Skills give engineers repeatable slash-command runbooks (
/audit-module) without bloating the context window at startup. -
MCP servers wire up live schemas and internal APIs using portable
${PLUGIN_ROOT}and${PLUGIN_DATA}variables. - Subagents isolate heavy security and architecture reviews into dedicated read-only personas.
-
Rules and Lifecycle Hooks pair context-aware guidance with deterministic
PreToolUsesafety gates.
Pick one workflow your team repeats every week—whether that is auditing Terraform modules, reviewing Firestore security rules, or verifying Kubernetes manifests—package it into a plugin.json bundle, and run agy plugin validate --strict.


Top comments (0)