Enabling My Vanity with Antigravity Sidecars

While I have done some real things in software over the course of my career, I'm definitely not one of the big name folks with a closet full of tailored T-shirts and a long line of keynote speaking invitations.

Even so, I still maintain a personal website, largely because having one makes me feel like a big deal.

Because I have one, it is obviously of utmost, earth-shattering importance that my readers get immediate updates the second I publish anything new. Over the years, I've written deep dives and architectural essays across the official Flutter and Dart publications. In theory, whenever an article goes live, I should immediately open my site repo, format a new entry for public/blog/index.html, write a brief editorial blurb that matches my site’s tone, update the RSS feed, push a branch, and open a pull request.

In practice, I'm a scatterbrain and forget.

When Google Antigravity introduced Sidecars—managed background processes that run alongside your development environment—I realized they could help me keep things up to date without relying on my proven-to-be-underwhelming ability to remember details. By the time I was done, I had learned a thing or two that seemed worth sharing.

What Exactly Is an Antigravity Sidecar?

In Antigravity, a sidecar is a long-running or periodic background process whose lifecycle is managed directly by the platform.

Unlike general CLI daemons or cron jobs hidden away in your operating system, you just create a directory inside your project (or your global user config) with a tiny configuration file:

my-project/
├── .agents/
│   └── sidecars/
│       └── my-sidecar/
│           ├── sidecar.json   <-- The declaration
│           └── watch.py       <-- The implementation

Antigravity monitors these directories, launches the processes when you open your workspace, handles automated restarts, and tracks process logs directly in your IDE's Auxiliary Pane and Output channels.

If you know how to write a script that runs in a loop or executes a periodic check, you already know how to write a sidecar.

The Architecture: Determinism vs. Agency

One of the first things I learned is the need to divide the work into subtasks and decide up front whether agentic or deterministic code will be better at getting them done correctly every time.

At first, I gave everything to the agent: "Please check the web for any articles I wrote, update my HTML, format git commits, and open a PR."

That approach is brittle. LLMs can hallucinate URLs, stumble over exact branch naming limits, or fail unpredictably when formatting raw RSS XML. In the end, my strategy was to let deterministic code handle the plumbing, and let the model handle the semantic synthesis of summaries and blurbs.

Antigravity Sidecar Pipeline: Deterministic vs. Agentic Workflow
Antigravity Sidecar Pipeline: Deterministic plumbing vs. Agentic synthesis

Deterministic code handles fetching the feeds, deduplication, Git branch enforcement, local logging, and GitHub App token minting. The LLM handles reading the full article text and condensing it into a tailored synopsis that matches my site’s specific voice.

Step 1: Declaring the Sidecar (sidecar.json)

To register a sidecar with Antigravity, you just create a directory inside .agents/sidecars/ (for project-scoped sidecars) or ~/.gemini/config/sidecars/ (for machine-wide sidecars). Inside, drop a sidecar.json file:

{
  "description": "Monitors blog.flutter.dev and dart.dev/blog for new articles and opens PRs via Gemini",
  "builtin": "schedule",
  "args": [
    "0 */6 * * *",
    "python3",
    "watch.py",
    "--once"
  ],
  "env": {
    "GEMINI_MODEL": "gemini-3.8-flash"
  }
}

Instead of running a persistent daemon that idly eats memory between runs, Antigravity’s built-in scheduler acts as the cron manager. Antigravity triggers python3 watch.py --once directly inside the sidecar folder every six hours, lets it perform the scan and open any needed PRs, and then terminates cleanly.

Security by Default: Explicit Activation

For safety, Antigravity disables all sidecars by default. Simply cloning an open-source project from GitHub or opening an unfamiliar repository will never quietly spin up background processes on your machine without your permission.

To activate your sidecar, you explicitly opt in by adding it to your global configuration file (~/.gemini/config/config.json):

{
  "sidecars": {
    "blog-watcher": {
      "enabled": true
    }
  }
}

Once enabled, Antigravity registers the task, and it surfaces directly under Scheduled Tasks in the left sidebar—where you can inspect its schedule, view its status, or click Run Now to trigger an on-demand check anytime.

Step 2: The Deterministic Engine

The core loop in Python is straightforward. Every six hours, it pulls the Atom feeds for the designated developer blogs:

# Monitored feeds
FEEDS = [
    {"name": "Flutter Blog", "url": "https://blog.flutter.dev/feed.xml"},
    {"name": "Dart Blog", "url": "https://dart.dev/blog/feed.xml"},
]

It parses the XML, checks for matching author tags, and cross-references the links against two things:

  1. Slugs already indexed in public/blog/index.html.
  2. Branches with active, open PRs on GitHub (via gh pr list --state open).

That second check is crucial for any background bot:

def is_branch_or_pr_pending(repo_root: Path, branch: str) -> bool:
    """Avoid re-running or spamming GitHub if a PR is already in review."""
    pr_check = subprocess.run(
        ["gh", "pr", "list", "--state", "open", "--json", "headRefName"],
        cwd=repo_root,
        capture_output=True,
        text=True,
        env=os.environ,
    )
    if pr_check.returncode == 0 and pr_check.stdout.strip():
        prs = json.loads(pr_check.stdout)
        if any(p.get("headRefName") == branch for p in prs):
            return True
    return False

If a PR is already open, the sidecar logs a polite notice and exits cleanly, waiting for the next scheduled execution. No duplicate branches, no merge conflicts, and no wasted API tokens.

Because background sidecars run quietly while you work on other things, I also added a rotating log file (watcher.log) right in the sidecar directory. Whenever I want to know when it last ran or what it detected, I can just click the log file in my editor.

Step 3: Bringing in an LLM

Once the deterministic engine finds an article that is genuinely new and unaddressed, it calls in the LLM.

Obviously, I have my own stylistic flair that demands new, customized summaries! On my website, blog entries don't use the raw marketing teaser from the feed; they have short, tailored summaries that frame what the post is about. Writing those summaries is text-based, generative work—the exact kind of task where modern models excel.

Using the Google GenAI SDK, I hand the model the article's raw text along with explicit style constraints:

from google import genai

client = genai.Client()

prompt = f"""You are an assistant for Andrew Brogdon's personal website (redbrogdon.dev).
The website has a refined, thoughtful tone for technical essays and deep dives.

Write a 1-2 sentence editorial summary for this new article to appear on the site.
Title: {article['title']}
URL: {article['url']}
Content:
{article['summary']}

Respond in JSON with:
{{
  "description": "1-2 sentence description matching site tone",
  "pr_summary": "Detailed summary explaining why this PR is being created"
}}
"""

response = client.models.generate_content(
    model="gemini-3.8-flash",
    contents=prompt,
    config={"response_mime_type": "application/json"},
)

With structured JSON output enabled, the model returns clean, parseable data—giving me both the editorial blurb for the HTML template and an informative PR description.

Step 4: A Bot with a Real Identity (GitHub Apps)

When you automate pull requests from a background script, you don't want the commits and PRs to look like you opened them by hand. You want clear provenance: an official bot badge so you can easily see which PRs were generated autonomously.

Rather than relying on brittle personal access tokens (PATs), you can configure the sidecar to authenticate as a GitHub App installation. This approach gives you the ability to strictly limit what the bot is allowed to do on GitHub (for instance, granting write access only to contents and pull requests while keeping everything else locked down), in addition to providing fine-grained repository permissions and short-lived access tokens:

import jwt  # PyJWT

# 1. Sign a 10-minute JWT using RS256 and the App's private key
now = int(time.time())
payload = {
    "iat": now - 60,
    "exp": now + 600,
    "iss": os.environ["GITHUB_APP_CLIENT_ID"],
}
jwt_token = jwt.encode(payload, private_key_pem, algorithm="RS256")

# 2. Exchange the JWT for a short-lived repository installation token
# POST https://api.github.com/app/installations/{installation_id}/access_tokens
# -> Returns an installation token starting with ghs_...

When creating the branch and commit, the script configures Git's environment variables to reflect the bot:

GIT_AUTHOR_NAME="redbrogdon-antigravity[bot]"
GIT_AUTHOR_EMAIL="<app-id>+redbrogdon-antigravity[bot]@users.noreply.github.com"

When pushing over HTTPS, Git authenticates using x-access-token:{token}.

The result? When the PR lands on GitHub, it carries the official GitHub App verification badge:

Screenshot of GitHub Pull Request #4 showing verified bot badge and Gemini-authored editorial description
Pull Request generated autonomously by the redbrogdon-antigravity bot

Conclusions and Some Additional Lessons Learned

Will all those steps done, I've got a working sidecar that helps keep my site up to date! In addition to the stuff described above, I learned a few other best practices along the way:

  1. Constrain the Agency: Don't let an agent guess at git commands or remote URLs. Hardcode your business rules—like capping branch names to ≤ 25 characters (bot/blog-<slug>) or checking if an open PR already exists—directly into your execution script.
  2. Sidecars Are Just Clean Unix Processes: There’s no proprietary runtime lock-in. An Antigravity sidecar is simply a process you define in sidecar.json. You can write it in Python, Dart, Go, Node, or Bash.
  3. Log for Asynchronous Peace of Mind: Because background sidecars run while you're focused on writing code in the foreground, always add a rotating log file (watcher.log). Being able to glance at a local log file and see exactly when the last cycle ran and what it found gives you total confidence in your background helpers.

What Will You Automate?

My next task for this sidecar is figuring out how to check YouTube and other sources for media appearances, podcasts, and talks so my media page stays just as effortlessly current.

Which brings me to you: what's your next step going to be?

Sidecars aren't just for monitoring RSS feeds—you can build one for almost any background task you'd rather not do by hand. If you're looking for a place to start, here are a few ideas worth building:

  • Dependency Drift Watcher: Periodically run dart pub outdated, have an LLM scan the changelogs for breaking changes, and open a summary issue or PR for safe minor updates.
  • API Contract Drift Detector: Poll your backend's OpenAPI or GraphQL schema and automatically open a PR updating client bindings before breaking changes hit staging.
  • Continuous Docstring & Sample Validator: Run a quiet background linter across modified files to ensure public API classes have up-to-date doc comments and compile-tested code snippets.

If you have developer maintenance tasks you keep forgetting to do, turn them into a sidecar.

Next Steps & Resources