Back to blog

Wednesday, August 19, 2026

Cross-Platform Agent Skills: Writing Universal SKILL.md for Antigravity, OpenCode & Claude Code

cover

The fragmentation of AI agent tooling has long been a source of developer friction. For years, every coding harness demanded its own proprietary configuration syntax: Claude had custom tool prompts, Gemini CLI used experimental extensions, OpenCode parsed bespoke JSON manifests, and Cursor maintained separate rule directives.

That era has ended. In 2026, the ecosystem converged on a clean, file-based open specification: Agent Skills powered by SKILL.md.

With Google officially retiring Gems in favor of Skills and open-source harnesses standardizing on the Agent Skills standard, you can now build a specialized capability once and deploy it across Google Antigravity (agy), OpenCode, Anthropic's Claude Code, Cursor, and GitHub Copilot without changing a single line of instruction.

However, "portable in theory" and "portable in production" are two different things. Subtle differences in discovery paths, path resolution, router models, and script execution environments can break a skill when moving between CLI harnesses.

This in-depth guide covers the technical nuances of building truly universal, cross-platform Agent Skills in 2026.


The State of Agent Portability in 2026

The convergence on SKILL.md mirrors the historic standardization of the Model Context Protocol (MCP) for tool calling:

┌──────────────────────────────────────────────────────────────┐
│                    Universal Skill Folder                    │
│                                                              │
│   my-skill/                                                  │
│   ├── SKILL.md        (Standard YAML Frontmatter + Markdown) │
│   ├── scripts/        (Portable Python / POSIX Sh)           │
│   ├── references/     (Static Markdown / Cheat sheets)       │
│   └── templates/      (Jinja / Handlebars / Text templates)  │
└──────────────────────────────┬───────────────────────────────┘
                               │
       ┌───────────────────────┼───────────────────────┐
       ▼                       ▼                       ▼
┌──────────────┐       ┌──────────────┐       ┌────────────────┐
│  Antigravity │       │   OpenCode   │       │  Claude Code   │
│ CLI & Desktop│       │  CLI & Web   │       │   CLI Engine   │
│  (Google AI) │       │ (Open Source)│       │  (Anthropic)   │
└──────────────┘       └──────────────┘       └────────────────┘

When Anthropic first introduced the folder-based skill structure, it established three core principles:

  1. Plaintext Primacy: Skills must live directly on the filesystem as standard Markdown and scripts.
  2. Progressive Disclosure: Agents inspect metadata first, loading full instructions and scripts only when relevant.
  3. Zero Runtime Lock-in: A skill requires no dedicated compilation step or vendor SDK.

Google's Antigravity runtime, the OpenCode foundation, and IDE vendors adopted the same mental model. While each harness may offer vendor-specific extensions (such as Antigravity's subagent delegation or OpenCode's sandboxed container runners), the baseline specification remains universally compatible.


Discovery Paths & Precedence Comparison

Every agent runtime resolves skills by scanning local project directories and user-level global configuration folders. Local project skills take precedence over global skills, allowing teams to check project-specific workflows directly into Git repositories.

Here is where each platform looks for your skills:

Runtime / HarnessProject-Level Discovery PathGlobal / User-Level Discovery Path
Antigravity CLI (agy) & IDE.agents/skills/ or .gemini/skills/~/.gemini/antigravity-cli/skills/ or ~/.agents/skills/
OpenCode.opencode/skills/ or .agents/skills/~/.config/opencode/skills/
Claude Code.claude/skills/ or .agents/skills/~/.claude/skills/
Cursor & GitHub Copilot.cursor/skills/ or .github/skills/~/.cursor/skills/

[!TIP] Universal Recommendation: Most modern harnesses (Antigravity v2+, OpenCode v1.4+, and Claude Code v2.1+) natively support the vendor-neutral .agents/skills/ directory at the project root. If you are configuring a shared repository, place your skills in .agents/skills/<skill-name>/ to avoid duplicating folders across vendor dotfiles.

Discovery Precedence Hierarchy

When multiple skills share the same name identifier, harnesses resolve collisions using a deterministic precedence hierarchy:

1. Active Session Override (--skill / CLI flag)
   └── 2. Project Local (.agents/skills/<name>/)
       └── 3. Workspace Shared (.cursor/ / .github/)
           └── 4. User Global (~/.gemini/... or ~/.claude/...)
               └── 5. Built-in Harness Bundles

Feature & Capability Matrix

While the core Markdown instruction parser is shared, platform runtimes differ in how they handle frontmatter keys, subagent execution, and token tiering.

Feature / CapabilityGoogle AntigravityOpenCodeClaude CodeCursor / Copilot
Core Metadata (name, description)SupportedSupportedSupportedSupported
Version Key (version: 1.2.0)SupportedSupportedSupportedIgnored
Model Override Key (model: ...)Supported (inherit, flash, pro)Supported (OpenRouter / Local IDs)Supported (sonnet, opus)Ignored
Tool Restrictions (tools: [...])SupportedSupportedSupportedPartial
Async Subagent InvocationNative (invoke_subagent)Native (Agent forks)Native (Sub-tasks)Linear Only
Progressive Disclosure TiersFull 3-TierFull 3-TierFull 3-Tier2-Tier (Prompt injection)
Max Recommended Prompt Tokens5,0004,0005,0002,000

Understanding the 3-Tier Progressive Disclosure Model

All major harnesses implement progressive disclosure to prevent token waste and context dilution:

[ Tier 1: Catalog ] ──▶ Name + Description loaded at session boot (~50-100 tokens)
                                 │
                                 ▼ (Trigger condition matched by router)
[ Tier 2: Instructions ] ──▶ SKILL.md body loaded into active context (<5,000 tokens)
                                 │
                                 ▼ (Agent determines script / file is needed)
[ Tier 3: Resources ] ──▶ Scripts, templates, and references read on demand
  1. Tier 1 (Catalog Scan): During initialization, the harness indexes only the YAML frontmatter (name and description).
  2. Tier 2 (Instruction Loading): When the user prompt matches a skill's description, the agent executes a file read on SKILL.md to ingest the core instructions.
  3. Tier 3 (Resource Execution): Supporting assets inside scripts/, templates/, or references/ are never dumped into context automatically. The agent reads or executes them on demand.

Authoring Universal Skills: The Golden Rules

To guarantee that your skill executes identically regardless of whether the developer invokes it via agy, opencode, or claude, follow these three golden architectural rules.

1. Cross-Shell Script Portability

Never assume the host environment uses Bash 5, Zsh, or GNU utilities. macOS defaults to BSD tools and older Bash, while minimal Docker containers in CI environments often ship with POSIX ash or sh.

❌ Bad: Non-portable Bashism
#!/usr/bin/env bash
declare -A MAP=( [key]="value" ) # Fails on default macOS bash (v3.2)
source "${0%/*}/helpers.sh"

✅ Good: POSIX-compliant shell wrapper or Python 3 standard library
#!/usr/bin/env python3
# Standard library only: sys, os, subprocess, json, argparse

[!IMPORTANT] The Zero-Dependency Python Rule: For complex scripts, write standalone Python 3 scripts relying strictly on the Python Standard Library (argparse, json, subprocess, re, pathlib, urllib.request). Do not require pip install or external virtualenvs unless the skill includes a self-bootstrapping verification step.

2. Relative Path Referencing vs. Absolute Path Traps

Harnesses execute commands from the developer's current working directory (the project root), not from the directory where SKILL.md resides.

If your skill attempts to execute ./scripts/my-script.py, it will fail unless the user happens to run the agent from inside the skill folder.

The Universal Path Resolution Pattern

When writing instructions inside SKILL.md, instruct the agent to locate helper scripts relative to the skill directory or via dynamic discovery:

### Executing Helper Scripts

Run the bundled script using Python 3:

```bash
# Locate the script from the skill directory:
python3 $(find . -path "*/skills/release-notes/scripts/summarize_git.py" | head -n 1) --from-tag "$(git describe --tags --abbrev=0 2>/dev/null || echo '')"
```

In your Python scripts, compute paths relative to the script file itself:

import os
import sys

# Compute the skill root directory regardless of current working directory
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_ROOT = os.path.dirname(SCRIPT_DIR)
TEMPLATES_DIR = os.path.join(SKILL_ROOT, "templates")
REFERENCES_DIR = os.path.join(SKILL_ROOT, "references")

3. Description Engineering for Heterogeneous Router Models

Your description frontmatter is the only text the model evaluates during Tier 1 indexing. Different router models (Claude 3.7/4 Sonnet, Gemini 3.5 Flash, GPT-5, Qwen 2.5/3) have distinct trigger sensitivities.

[!WARNING] Vague descriptions cause routing failure.

  • description: Helps with Git and releases.
  • description: Release note generator.

The agent will ignore vague skills 80% of the time because it cannot determine exact activation boundaries.

The Triple-Trigger Description Formula

A robust, universally discoverable description must contain three components:

  1. Primary Capability: What the skill does.
  2. Key Triggers / Synonyms: Exact keywords, verbs, and common user phrases.
  3. Exclusion / Boundary Condition: When not to activate the skill.
---
name: release-notes-generator
description: |
  Generates production changelogs and release notes from Git commit history, PRs, and conventional commit tags.
  Use this skill whenever the user asks to:
  - "create release notes"
  - "generate changelog"
  - "prepare release summary for vX.Y.Z"
  - "summarize changes since last tag"
  Do NOT use for single commit message generation (use standard git commit workflows instead).
---

Step-by-Step Walkthrough: Building a Universal "Release Notes Generator" Skill

Let's build a production-grade, universal skill that analyzes Git commit history, categorizes changes according to Conventional Commits, and produces release notes formatted for GitHub Releases or internal distribution.

1. Skill Directory Structure

Create the folder structure inside .agents/skills/release-notes/:

.agents/skills/release-notes/
├── SKILL.md
├── scripts/
│   └── summarize_git.py
├── templates/
│   └── release_template.md
└── references/
    └── conventional_commits.md

2. The Universal SKILL.md

Create .agents/skills/release-notes/SKILL.md:

---
name: release-notes
description: |
  Automates the generation of structured, categorized release notes and changelogs from Git commit logs.
  Triggers on requests to "draft release notes", "generate changelog", "summarize commits since last release",
  or "prepare vX.Y.Z release notes". Uses Conventional Commits standards to group breaking changes,
  features, bug fixes, and performance improvements.
---

# Release Notes Generator

This skill extracts git history between two tags or commits, parses conventional commit conventions, and formats production-ready release notes using standard templates.

## Workflow

Follow these steps sequentially to generate release notes:

### Step 1: Inspect Git Repository & Identify Range

Determine the start and end revisions:
1. Find the latest Git tag:
   ```bash
   git describe --tags --abbrev=0 2>/dev/null || echo "INITIAL"
  1. If the user specified a version or range (e.g., v1.2.0..v1.3.0 or v1.2.0..HEAD), use that range.
  2. If no prior tag exists, summarize all commits up to HEAD.

Step 2: Extract and Categorize Commits

Execute the bundled Python extraction script to collect structured JSON metadata:

# Locate the script relative to skill paths
python3 $(find . -path "*/skills/release-notes/scripts/summarize_git.py" | head -n 1) --from-tag "$(git describe --tags --abbrev=0 2>/dev/null || echo '')" --to-ref "HEAD"

If the Python script is unavailable or returns an error, fall back to raw Git log inspection:

git log --pretty=format:"%h|%s|%an|%ad" --date=short $(git describe --tags --abbrev=0 2>/dev/null)..HEAD

Step 3: Parse and Classify Changes

Classify each commit into one of the following sections according to Conventional Commits:

PrefixSection TitleDescription
feat:🚀 Features & EnhancementsNew functionality or capabilities
fix:🐛 Bug FixesResolutions to known defects
perf:⚡ Performance ImprovementsSpeed, memory, or throughput gains
refactor:🛠️ Refactoring & ArchitectureInternal structural improvements
docs:📚 Documentation UpdatesDoc changes, examples, or tutorials
test:, ci:🤖 Maintenance & CI/CDTesting, workflows, and dependency bumps
BREAKING CHANGE:⚠️ Breaking ChangesBackwards-incompatible API or config changes

Consult references/conventional_commits.md for edge-case classifications.

Step 4: Assemble the Release Notes

  1. Load templates/release_template.md.
  2. Populate the release version, release date, and summary overview.
  3. Group the classified commits into bullet points, cleaning up noisy commit messages into clear, human-readable explanations.
  4. Highlight any breaking changes prominently with migration guidance.
  5. Present the final Markdown output to the user and offer to write it to CHANGELOG.md or a GitHub Release draft.

---

### 3. The Extraction Script (`summarize_git.py`)

Create `.agents/skills/release-notes/scripts/summarize_git.py` using pure Python 3 standard library:

```python
#!/usr/bin/env python3
"""
Universal Git Commit Extractor & Categorizer for Agent Skills.
Requires Python 3.8+ with zero third-party dependencies.
"""

import argparse
import json
import re
import subprocess
import sys


CONVENTIONAL_PATTERN = re.compile(
    r"^(?P<type>feat|fix|perf|refactor|docs|test|ci|chore|style|build)"
    r"(?:\((?P<scope>[^\)]+)\))?"
    r"(?P<breaking>!)?"
    r":\s*(?P<subject>.+)$",
    re.IGNORECASE,
)


def run_command(cmd):
    """Executes a shell command and returns standard output."""
    try:
        res = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=True,
            shell=isinstance(cmd, str),
        )
        return res.stdout.strip()
    except subprocess.CalledProcessError as e:
        sys.stderr.write(f"Command failed: {cmd}\nError: {e.stderr}\n")
        return ""


def get_commit_range(from_tag, to_ref):
    """Calculates git revision range."""
    if not from_tag:
        # Check if there is an existing tag
        latest_tag = run_command(["git", "describe", "--tags", "--abbrev=0"])
        from_tag = latest_tag if latest_tag else ""

    if from_tag:
        return f"{from_tag}..{to_ref}"
    return to_ref


def extract_commits(rev_range):
    """Extracts commit logs formatted as JSON-friendly dictionaries."""
    log_format = "%H%x1f%h%x1f%s%x1f%b%x1f%an%x1f%ad"
    cmd = ["git", "log", f"--pretty=format:{log_format}", "--date=short"]
    
    if ".." in rev_range:
        cmd.append(rev_range)
    else:
        cmd.extend(["-n", "50", rev_range])

    raw_output = run_command(cmd)
    if not raw_output:
        return []

    commits = []
    for line in raw_output.split("\n"):
        parts = line.split("\x1f")
        if len(parts) < 6:
            continue

        full_hash, short_hash, subject, body, author, date = parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]
        
        # Parse conventional commit syntax
        match = CONVENTIONAL_PATTERN.match(subject)
        is_breaking = bool(match and match.group("breaking")) or ("BREAKING CHANGE" in body)
        
        c_type = match.group("type").lower() if match else "other"
        c_scope = match.group("scope") if match else None
        c_subject = match.group("subject") if match else subject

        commits.append({
            "hash": short_hash,
            "full_hash": full_hash,
            "raw_subject": subject,
            "type": c_type,
            "scope": c_scope,
            "subject": c_subject,
            "body": body.strip(),
            "author": author,
            "date": date,
            "is_breaking": is_breaking,
        })

    return commits


def categorize_commits(commits):
    """Groups commits by category for release note drafting."""
    categorized = {
        "breaking": [],
        "features": [],
        "fixes": [],
        "performance": [],
        "refactoring": [],
        "documentation": [],
        "maintenance": [],
        "other": [],
    }

    type_mapping = {
        "feat": "features",
        "fix": "fixes",
        "perf": "performance",
        "refactor": "refactoring",
        "docs": "documentation",
        "test": "maintenance",
        "ci": "maintenance",
        "chore": "maintenance",
        "build": "maintenance",
    }

    for c in commits:
        if c["is_breaking"]:
            categorized["breaking"].append(c)
        
        cat = type_mapping.get(c["type"], "other")
        categorized[cat].append(c)

    return categorized


def main():
    parser = argparse.ArgumentParser(description="Extract Git commits for Agent Skill release notes.")
    parser.add_argument("--from-tag", default="", help="Starting tag or revision")
    parser.add_argument("--to-ref", default="HEAD", help="Ending revision (default: HEAD)")
    parser.add_argument("--json", action="store_true", default=True, help="Output JSON structure")
    args = parser.parse_args()

    rev_range = get_commit_range(args.from_tag, args.to_ref)
    commits = extract_commits(rev_range)
    categorized = categorize_commits(commits)

    result = {
        "range": rev_range,
        "total_commits": len(commits),
        "categories": categorized,
    }

    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()

4. Supporting Templates and Reference Files

Template: .agents/skills/release-notes/templates/release_template.md

# Release Notes - ${VERSION} (${DATE})

## 🌟 Overview
${OVERVIEW_SUMMARY}

---

## ⚠️ Breaking Changes
${BREAKING_CHANGES_LIST}

## 🚀 Features & Improvements
${FEATURES_LIST}

## 🐛 Bug Fixes
${BUG_FIXES_LIST}

## ⚡ Performance Improvements
${PERF_LIST}

## 🛠️ Internal & Maintenance
${MAINTENANCE_LIST}

---
**Full Changelog**: [${PREVIOUS_TAG}...${VERSION}](https://github.com/${REPO_OWNER}/${REPO_NAME}/compare/${PREVIOUS_TAG}...${VERSION})

Reference: .agents/skills/release-notes/references/conventional_commits.md

# Conventional Commits Reference (v1.0.0)

Conventional Commits provide lightweight conventions on top of commit messages:

`<type>[optional scope]: <description>`

### Recognized Types
- **feat**: Introduces a new user-facing feature.
- **fix**: Patches a bug or regression.
- **perf**: Changes that improve execution performance.
- **refactor**: Code restructuring without bug fixes or new features.
- **docs**: Markdown or documentation updates only.
- **test**: Adding or correcting automated tests.
- **ci**: Pipeline and workflow script modifications.
- **chore**: Routine maintenance tasks or dependencies.

### Breaking Change Syntax
1. An exclamation mark before the colon: `feat(api)!: drop deprecated v1 auth endpoints`
2. An entry in the commit footer: `BREAKING CHANGE: The 'legacyAuth' flag has been removed.`

Testing Across All Three Harnesses

Once your universal skill folder is assembled in .agents/skills/release-notes/, test execution across each CLI runtime to verify Tier 1 discovery and Tier 2 execution:

1. Google Antigravity CLI (agy)

Run agy from your project root:

# Start interactive agent session
agy

# Prompt the agent
> Draft the release notes for our upcoming v2.4.0 release based on the latest commits

Verification: In Antigravity, check that agy lists release-notes under active skills, reads SKILL.md, invokes the Python extraction script, and formats the output according to release_template.md.

2. OpenCode CLI (opencode)

Run OpenCode:

opencode "Generate changelog for release v2.4.0 using the release-notes skill"

Verification: OpenCode scans .agents/skills/ or .opencode/skills/, runs the headless subagent, executes summarize_git.py, and returns the markdown diff directly in the terminal.

3. Claude Code (claude)

Run Claude Code:

claude "Prepare release notes for HEAD compared to last tag"

Verification: Claude Code indexes .agents/skills/, outputs its execution plan, parses the conventional commit structures, and generates the changelog.


Packaging and Distribution Strategies

To distribute your custom skills across multiple developer machines or team repositories, consider three proven strategies:

┌─────────────────────────────────────────────────────────────┐
│                 Skill Distribution Models                   │
├──────────────────────────────┬──────────────────────────────┤
│ 1. Git Submodule / Dotfiles  │ Shared across company repos  │
│ 2. Symlinked Central Cache   │ Developer ~/.config setup    │
│ 3. Skill Packs Archive       │ Packaged tarball distribution│
└──────────────────────────────┴──────────────────────────────┘

Strategy 1: Git Symlinking for Local Developers

If you manage a personal dotfiles repository with reusable skills, symlink your central skills directory into project roots:

# Symlink your global skills into any active repository
mkdir -p .agents
ln -s ~/.gemini/antigravity-cli/skills .agents/skills

Strategy 2: Team-Wide Git Submodules

For engineering teams maintaining 50+ microservices, store company-standard skills in a dedicated repository (github.com/your-org/agent-skills) and attach it as a Git submodule:

git submodule add https://github.com/your-org/agent-skills.git .agents/skills

Whenever standard operational procedures or release checklists change, team members run git submodule update --remote to immediately sync agent capabilities.


Summary Checklist for Universal Skills

Before committing a new skill to your repository, run through this final quality assurance checklist:

  • Standard Directory: Stored under .agents/skills/<skill-name>/ with a valid SKILL.md.
  • Precise Description: Description contains clear activation verbs, exact user triggers, and boundary conditions.
  • No Hardcoded Paths: Scripts locate files via dynamic relative paths (os.path.dirname(__file__)).
  • Zero Extra Dependencies: Helper scripts run on standard Python 3.8+ or POSIX sh.
  • Progressive Disclosure: Large reference docs and templates are separated into references/ and templates/ rather than embedded directly in SKILL.md.
  • Multi-Harness Tested: Tested across at least two different CLI runners (agy, opencode, claude).

By authoring universal SKILL.md packages, you future-proof your agent workflows against framework churn and enable your entire team to work with the tools they prefer.


Related Reading: