DEV Community

Preecha
Preecha

Posted on

How to Convert One AI Agent to Work in 10 Different IDEs

Convert One AI Agent File for 10 IDEs with Bash

Maintain one canonical Markdown agent definition, then use Bash to generate the formats required by Claude Code, Cursor, Aider, Windsurf, GitHub Copilot, and other AI development tools.

Try Apidog today

TL;DR

The conversion pipeline has three stages:

  1. Parse YAML frontmatter with get_field(), get_body(), and to_kebab().
  2. Transform the agent into tool-specific formats with convert.sh.
  3. Copy the generated files into the expected locations with install.sh.

The result is a write-once workflow:

Canonical agent.md
        |
        v
    convert.sh
        |
        +-- Claude Code: .md
        +-- Cursor: .mdc
        +-- Aider: CONVENTIONS.md
        +-- Windsurf: .windsurfrules
        +-- Antigravity: SKILL.md
        +-- OpenClaw: SOUL.md + AGENTS.md + IDENTITY.md
        |
        v
     install.sh
Enter fullscreen mode Exit fullscreen mode

This is useful when the same agent needs to support API development workflows with Apidog integration, automated testing, architecture reviews, or other specialized tasks across multiple IDEs.

The Canonical Agent Format

Every agent in The Agency uses a Markdown file with two sections:

  1. YAML frontmatter containing metadata.
  2. A Markdown body containing the agent instructions.

Create an example at testing/api-tester.md:

---
name: API Tester
description: "Specialized in API testing with Apidog, Postman, and automated validation"
color: purple
emoji: ๐Ÿงช
vibe: Breaks APIs before users do.
---

# API Tester Agent Personality

You are **API Tester**, an expert in API validation.

## Identity & Memory

- Role: API testing specialist
- Personality: Thorough, skeptical, evidence-focused
Enter fullscreen mode Exit fullscreen mode

The conversion scripts need to:

  • Extract fields such as name and description.
  • Remove the frontmatter when a target only needs the Markdown body.
  • Convert the agent name into a safe filename.
  • Write the result to each tool's required path.

A practical repository layout looks like this:

agency-agents/
โ”œโ”€โ”€ engineering/
โ”‚   โ””โ”€โ”€ backend-architect.md
โ”œโ”€โ”€ design/
โ”‚   โ””โ”€โ”€ ui-designer.md
โ”œโ”€โ”€ testing/
โ”‚   โ””โ”€โ”€ api-tester.md
โ”œโ”€โ”€ marketing/
โ”œโ”€โ”€ sales/
โ”œโ”€โ”€ scripts/
โ”‚   โ”œโ”€โ”€ parse-frontmatter.sh
โ”‚   โ”œโ”€โ”€ convert.sh
โ”‚   โ””โ”€โ”€ install.sh
โ””โ”€โ”€ integrations/
Enter fullscreen mode Exit fullscreen mode

Step 1: Parse YAML Frontmatter

Create scripts/parse-frontmatter.sh:

#!/usr/bin/env bash
#
# parse-frontmatter.sh โ€” Extract fields and content from agent files
#

set -euo pipefail

# Extract one field from the first YAML frontmatter block.
# Usage: get_field <field> <file>
get_field() {
  local field="$1"
  local file="$2"

  awk -v field="$field" '
    /^---$/ {
      frontmatter_blocks++
      next
    }

    frontmatter_blocks == 1 && $0 ~ "^" field ":[[:space:]]+" {
      sub("^" field ":[[:space:]]+", "")
      print
      exit
    }
  ' "$file"
}

# Print everything after the closing frontmatter delimiter.
# Usage: get_body <file>
get_body() {
  local file="$1"

  awk '
    /^---$/ {
      frontmatter_blocks++
      next
    }

    frontmatter_blocks >= 2 {
      print
    }
  ' "$file"
}

# Convert a display name to a kebab-case filename.
# Usage: to_kebab "API Tester"
to_kebab() {
  printf '%s' "$1" |
    tr '[:upper:]' '[:lower:]' |
    sed 's/[^a-z0-9]/-/g' |
    sed 's/--*/-/g' |
    sed 's/^-//' |
    sed 's/-$//'
}

if [[ "${1:-}" == "--demo" ]]; then
  agent_file="${2:-test-agent.md}"

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  printf 'File: %s\n' "$agent_file"
  printf 'Name: %s\n' "$name"
  printf 'Description: %s\n' "$description"
  printf 'Slug: %s\n' "$slug"
  printf '%s\n' "---"
  printf '%s\n' "Body preview:"

  get_body "$agent_file" | head -10
fi
Enter fullscreen mode Exit fullscreen mode

Make the script executable and test it:

chmod +x scripts/parse-frontmatter.sh

scripts/parse-frontmatter.sh \
  --demo \
  engineering/backend-architect.md
Enter fullscreen mode Exit fullscreen mode

Expected output:

File: engineering/backend-architect.md
Name: Backend Architect
Description: Senior backend architect specializing in scalable system design...
Slug: backend-architect
---
Body preview:
# Backend Architect Agent Personality

You are **Backend Architect**, a senior backend architect...
Enter fullscreen mode Exit fullscreen mode

Understand the parser's limits

This parser intentionally handles simple, flat frontmatter such as:

name: API Tester
description: Tests API behavior and contracts
Enter fullscreen mode Exit fullscreen mode

It is not a complete YAML parser. Use a dedicated YAML tool if your metadata contains:

  • Multiline values
  • Nested objects
  • Arrays
  • Escaped colons
  • Complex quoting rules

For a controlled agent repository with predictable frontmatter, the small awk implementation is often sufficient.

Step 2: Convert Agents for Claude Code

Claude Code uses Markdown agent files, so the conversion is a direct copy:

convert_claude_code() {
  local agent_file="$1"
  local destination="$OUT_DIR/claude-code"

  mkdir -p "$destination"
  cp "$agent_file" "$destination/"

  printf '  Claude Code: %s\n' "$(basename "$agent_file")"
}
Enter fullscreen mode Exit fullscreen mode

The generated structure is:

integrations/
โ””โ”€โ”€ claude-code/
    โ”œโ”€โ”€ api-tester.md
    โ””โ”€โ”€ backend-architect.md
Enter fullscreen mode Exit fullscreen mode

During installation, these files are copied to:

~/.claude/agents/
Enter fullscreen mode Exit fullscreen mode

Step 3: Convert Agents for Cursor

Cursor rules use .mdc files with frontmatter containing a description.

Add this converter:

convert_cursor() {
  local agent_file="$1"
  local name
  local description
  local slug
  local output

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  output="$OUT_DIR/cursor/.cursor/rules/agency-${slug}.mdc"
  mkdir -p "$(dirname "$output")"

  {
    printf '%s\n' "---"
    printf 'description: Agency agent: %s\n' "$description"
    printf '%s\n' "---"
    get_body "$agent_file"
  } > "$output"

  printf '  Cursor: agency-%s.mdc\n' "$slug"
}
Enter fullscreen mode Exit fullscreen mode

Given this input:

---
name: API Tester
description: Specialized in API testing
---

# API Tester Agent

Validate API contracts, error responses, and edge cases.
Enter fullscreen mode Exit fullscreen mode

The converter generates:

---
description: Agency agent: Specialized in API testing
---

# API Tester Agent

Validate API contracts, error responses, and edge cases.
Enter fullscreen mode Exit fullscreen mode

The output path is:

integrations/cursor/.cursor/rules/agency-api-tester.mdc
Enter fullscreen mode Exit fullscreen mode

Step 4: Build Aider's CONVENTIONS.md

Aider uses a single CONVENTIONS.md file in the project root. Instead of creating one file per agent, concatenate all canonical agent files.

initialize_aider() {
  local output="$OUT_DIR/aider/CONVENTIONS.md"

  mkdir -p "$(dirname "$output")"

  {
    printf '%s\n\n' "# Agency Agents for Aider"
    printf '%s\n' "This file contains the generated Agency agent definitions."
  } > "$output"
}

convert_aider() {
  local agent_file="$1"
  local output="$OUT_DIR/aider/CONVENTIONS.md"

  {
    printf '\n%s\n\n' "---"
    cat "$agent_file"
  } >> "$output"

  printf '  Aider: appended %s\n' "$(basename "$agent_file")"
}
Enter fullscreen mode Exit fullscreen mode

Initialize the file once before appending agents:

initialize_aider

for agent_file in testing/*.md engineering/*.md; do
  [[ -f "$agent_file" ]] || continue
  convert_aider "$agent_file"
done
Enter fullscreen mode Exit fullscreen mode

Do not initialize the file inside convert_aider(), or each agent will overwrite the previous one.

Step 5: Build Windsurf's .windsurfrules

Windsurf also uses a combined project-level file:

initialize_windsurf() {
  local output="$OUT_DIR/windsurf/.windsurfrules"

  mkdir -p "$(dirname "$output")"

  printf '%s\n' "# Agency Agents for Windsurf" > "$output"
}

convert_windsurf() {
  local agent_file="$1"
  local output="$OUT_DIR/windsurf/.windsurfrules"

  {
    printf '\n%s\n\n' "---"
    cat "$agent_file"
  } >> "$output"

  printf '  Windsurf: appended %s\n' "$(basename "$agent_file")"
}
Enter fullscreen mode Exit fullscreen mode

The generated file is installed as:

./.windsurfrules
Enter fullscreen mode Exit fullscreen mode

Step 6: Convert Agents for Antigravity

Antigravity uses a SKILL.md file inside a dedicated directory for each agent:

convert_antigravity() {
  local agent_file="$1"
  local name
  local slug
  local output

  name="$(get_field "name" "$agent_file")"
  slug="$(to_kebab "$name")"

  output="$OUT_DIR/antigravity/skills/agency-${slug}/SKILL.md"
  mkdir -p "$(dirname "$output")"

  {
    printf '# Agency Agent: %s\n\n' "$name"
    get_body "$agent_file"
  } > "$output"

  printf '  Antigravity: agency-%s/SKILL.md\n' "$slug"
}
Enter fullscreen mode Exit fullscreen mode

The output structure is:

integrations/
โ””โ”€โ”€ antigravity/
    โ””โ”€โ”€ skills/
        โ””โ”€โ”€ agency-api-tester/
            โ””โ”€โ”€ SKILL.md
Enter fullscreen mode Exit fullscreen mode

Step 7: Convert Agents for OpenClaw

OpenClaw uses three files per agent:

  • SOUL.md for the full agent definition
  • AGENTS.md for capabilities
  • IDENTITY.md for identity metadata
convert_openclaw() {
  local agent_file="$1"
  local name
  local description
  local slug
  local output_dir

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  output_dir="$OUT_DIR/openclaw/agency-${slug}"
  mkdir -p "$output_dir"

  {
    printf '# %s\n\n' "$name"
    printf '%s\n\n' "$description"
    printf '%s\n\n' "---"
    get_body "$agent_file"
  } > "$output_dir/SOUL.md"

  cat > "$output_dir/AGENTS.md" <<EOF
# Agent Capabilities: $name

- Specialized expertise in its defined domain
- Deliverable-focused output
- Explicit success criteria

See SOUL.md for the complete agent definition.
EOF

  cat > "$output_dir/IDENTITY.md" <<EOF
# Identity: $name

- Name: $name
- Description: $description
- Source: The Agency
EOF

  printf '  OpenClaw: agency-%s/\n' "$slug"
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Assemble the Complete convert.sh

Create scripts/convert.sh:

#!/usr/bin/env bash
#
# convert.sh โ€” Convert canonical agents to tool-specific formats
#

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUT_DIR="$REPO_ROOT/integrations"

AGENT_DIRS=(
  engineering
  design
  testing
  marketing
  sales
)

get_field() {
  local field="$1"
  local file="$2"

  awk -v field="$field" '
    /^---$/ {
      frontmatter_blocks++
      next
    }

    frontmatter_blocks == 1 && $0 ~ "^" field ":[[:space:]]+" {
      sub("^" field ":[[:space:]]+", "")
      print
      exit
    }
  ' "$file"
}

get_body() {
  local file="$1"

  awk '
    /^---$/ {
      frontmatter_blocks++
      next
    }

    frontmatter_blocks >= 2 {
      print
    }
  ' "$file"
}

to_kebab() {
  printf '%s' "$1" |
    tr '[:upper:]' '[:lower:]' |
    sed 's/[^a-z0-9]/-/g' |
    sed 's/--*/-/g' |
    sed 's/^-//' |
    sed 's/-$//'
}

validate_agent() {
  local agent_file="$1"
  local name
  local description
  local slug

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  if [[ -z "$name" ]]; then
    printf 'ERROR: Missing name in %s\n' "$agent_file" >&2
    return 1
  fi

  if [[ -z "$description" ]]; then
    printf 'ERROR: Missing description in %s\n' "$agent_file" >&2
    return 1
  fi

  if [[ -z "$slug" ]]; then
    printf 'ERROR: Could not generate slug for %s\n' "$agent_file" >&2
    return 1
  fi
}

convert_claude_code() {
  local agent_file="$1"
  local destination="$OUT_DIR/claude-code"

  mkdir -p "$destination"
  cp "$agent_file" "$destination/"
}

convert_cursor() {
  local agent_file="$1"
  local name
  local description
  local slug
  local output

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  output="$OUT_DIR/cursor/.cursor/rules/agency-${slug}.mdc"
  mkdir -p "$(dirname "$output")"

  {
    printf '%s\n' "---"
    printf 'description: Agency agent: %s\n' "$description"
    printf '%s\n' "---"
    get_body "$agent_file"
  } > "$output"
}

initialize_aider() {
  local output="$OUT_DIR/aider/CONVENTIONS.md"

  mkdir -p "$(dirname "$output")"

  {
    printf '%s\n\n' "# Agency Agents for Aider"
    printf '%s\n' "This file contains the generated Agency agent definitions."
  } > "$output"
}

convert_aider() {
  local agent_file="$1"
  local output="$OUT_DIR/aider/CONVENTIONS.md"

  {
    printf '\n%s\n\n' "---"
    cat "$agent_file"
  } >> "$output"
}

initialize_windsurf() {
  local output="$OUT_DIR/windsurf/.windsurfrules"

  mkdir -p "$(dirname "$output")"
  printf '%s\n' "# Agency Agents for Windsurf" > "$output"
}

convert_windsurf() {
  local agent_file="$1"
  local output="$OUT_DIR/windsurf/.windsurfrules"

  {
    printf '\n%s\n\n' "---"
    cat "$agent_file"
  } >> "$output"
}

convert_antigravity() {
  local agent_file="$1"
  local name
  local slug
  local output

  name="$(get_field "name" "$agent_file")"
  slug="$(to_kebab "$name")"

  output="$OUT_DIR/antigravity/skills/agency-${slug}/SKILL.md"
  mkdir -p "$(dirname "$output")"

  {
    printf '# Agency Agent: %s\n\n' "$name"
    get_body "$agent_file"
  } > "$output"
}

convert_openclaw() {
  local agent_file="$1"
  local name
  local description
  local slug
  local output_dir

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  output_dir="$OUT_DIR/openclaw/agency-${slug}"
  mkdir -p "$output_dir"

  {
    printf '# %s\n\n' "$name"
    printf '%s\n\n' "$description"
    printf '%s\n\n' "---"
    get_body "$agent_file"
  } > "$output_dir/SOUL.md"

  cat > "$output_dir/AGENTS.md" <<EOF
# Agent Capabilities: $name

- Specialized expertise in its defined domain
- Deliverable-focused output
- Explicit success criteria

See SOUL.md for the complete agent definition.
EOF

  cat > "$output_dir/IDENTITY.md" <<EOF
# Identity: $name

- Name: $name
- Description: $description
- Source: The Agency
EOF
}

collect_agent_files() {
  local directory
  local agent_file

  for directory in "${AGENT_DIRS[@]}"; do
    for agent_file in "$REPO_ROOT/$directory"/*.md; do
      [[ -f "$agent_file" ]] || continue
      printf '%s\n' "$agent_file"
    done
  done
}

main() {
  local agent_file
  local name

  mkdir -p "$OUT_DIR"

  initialize_aider
  initialize_windsurf

  printf '%s\n' "Converting Agency agents..."

  while IFS= read -r agent_file; do
    validate_agent "$agent_file"

    name="$(get_field "name" "$agent_file")"
    printf 'Processing: %s\n' "$name"

    convert_claude_code "$agent_file"
    convert_cursor "$agent_file"
    convert_aider "$agent_file"
    convert_windsurf "$agent_file"
    convert_antigravity "$agent_file"
    convert_openclaw "$agent_file"
  done < <(collect_agent_files)

  printf '%s\n' "Conversion complete!"
  printf '  Claude Code: %s\n' "$OUT_DIR/claude-code/"
  printf '  Cursor: %s\n' "$OUT_DIR/cursor/.cursor/rules/"
  printf '  Aider: %s\n' "$OUT_DIR/aider/CONVENTIONS.md"
  printf '  Windsurf: %s\n' "$OUT_DIR/windsurf/.windsurfrules"
  printf '  Antigravity: %s\n' "$OUT_DIR/antigravity/skills/"
  printf '  OpenClaw: %s\n' "$OUT_DIR/openclaw/"
}

main "$@"
Enter fullscreen mode Exit fullscreen mode

Run the conversion:

chmod +x scripts/convert.sh
scripts/convert.sh
Enter fullscreen mode Exit fullscreen mode

Inspect the generated files before installing them:

find integrations -type f -print
Enter fullscreen mode Exit fullscreen mode

Example output:

integrations/claude-code/api-tester.md
integrations/cursor/.cursor/rules/agency-api-tester.mdc
integrations/aider/CONVENTIONS.md
integrations/windsurf/.windsurfrules
integrations/antigravity/skills/agency-api-tester/SKILL.md
integrations/openclaw/agency-api-tester/SOUL.md
integrations/openclaw/agency-api-tester/AGENTS.md
integrations/openclaw/agency-api-tester/IDENTITY.md
Enter fullscreen mode Exit fullscreen mode

Step 9: Install the Generated Files

Create scripts/install.sh:

#!/usr/bin/env bash
#
# install.sh โ€” Install generated agents into local tools
#

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
INTEGRATIONS_DIR="$REPO_ROOT/integrations"

install_claude_code() {
  local source_dir="$INTEGRATIONS_DIR/claude-code"
  local destination="$HOME/.claude/agents"

  mkdir -p "$destination"
  cp "$source_dir"/*.md "$destination/"

  printf 'Claude Code: installed agents into %s\n' "$destination"
}

install_cursor() {
  local source_dir="$INTEGRATIONS_DIR/cursor/.cursor/rules"
  local destination="$REPO_ROOT/.cursor/rules"

  mkdir -p "$destination"
  cp "$source_dir"/*.mdc "$destination/"

  printf 'Cursor: installed rules into %s\n' "$destination"
}

install_aider() {
  local source="$INTEGRATIONS_DIR/aider/CONVENTIONS.md"
  local destination="$REPO_ROOT/CONVENTIONS.md"

  if [[ -f "$destination" ]]; then
    cp "$destination" "${destination}.backup"
    printf 'Aider: backed up existing file to %s.backup\n' "$destination"
  fi

  cp "$source" "$destination"
  printf 'Aider: installed %s\n' "$destination"
}

install_windsurf() {
  local source="$INTEGRATIONS_DIR/windsurf/.windsurfrules"
  local destination="$REPO_ROOT/.windsurfrules"

  if [[ -f "$destination" ]]; then
    cp "$destination" "${destination}.backup"
    printf 'Windsurf: backed up existing file to %s.backup\n' "$destination"
  fi

  cp "$source" "$destination"
  printf 'Windsurf: installed %s\n' "$destination"
}

install_all() {
  install_claude_code
  install_cursor
  install_aider
  install_windsurf
}

case "${1:-all}" in
  claude-code)
    install_claude_code
    ;;
  cursor)
    install_cursor
    ;;
  aider)
    install_aider
    ;;
  windsurf)
    install_windsurf
    ;;
  all)
    install_all
    ;;
  *)
    printf 'Usage: %s [claude-code|cursor|aider|windsurf|all]\n' "$0" >&2
    exit 1
    ;;
esac
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x scripts/install.sh
Enter fullscreen mode Exit fullscreen mode

Install one integration:

scripts/install.sh cursor
Enter fullscreen mode Exit fullscreen mode

Or install all supported integrations:

scripts/install.sh all
Enter fullscreen mode Exit fullscreen mode

The script creates backups before replacing existing Aider or Windsurf project files. Review those files before deleting the backups.

Format Comparison

Tool Format Scope Conversion
Claude Code .md User-wide: ~/.claude/agents/ Copy as-is
Cursor .mdc Project: .cursor/rules/ Add description frontmatter
Aider CONVENTIONS.md Project root Concatenate all agents
Windsurf .windsurfrules Project root Concatenate all agents
GitHub Copilot .md User-wide: ~/.github/agents/ Copy as-is
Antigravity SKILL.md User-wide: ~/.gemini/antigravity/ Create one skill directory per agent
OpenClaw SOUL.md, AGENTS.md, IDENTITY.md User-wide: ~/.openclaw/ Split each agent into three files
Gemini CLI Extension files User-wide: ~/.gemini/extensions/ Generate a manifest and skills
OpenCode .md Project: .opencode/agents/ Copy as-is
Qwen Code .md Project: .qwen/agents/ Copy as a subagent

Before adding a converter, verify the current format and installation path expected by the target tool.

Add Support for Another Tool

Use this template:

convert_your_tool() {
  local agent_file="$1"
  local name
  local description
  local slug
  local output

  name="$(get_field "name" "$agent_file")"
  description="$(get_field "description" "$agent_file")"
  slug="$(to_kebab "$name")"

  output="$OUT_DIR/your-tool/agency-${slug}.ext"
  mkdir -p "$(dirname "$output")"

  {
    printf '# %s\n\n' "$name"
    printf '%s\n\n' "$description"
    get_body "$agent_file"
  } > "$output"

  printf '  YourTool: agency-%s.ext\n' "$slug"
}
Enter fullscreen mode Exit fullscreen mode

Then call it from the main conversion loop:

while IFS= read -r agent_file; do
  validate_agent "$agent_file"

  convert_claude_code "$agent_file"
  convert_cursor "$agent_file"
  convert_your_tool "$agent_file"
done < <(collect_agent_files)
Enter fullscreen mode Exit fullscreen mode

When implementing a new converter, answer these questions first:

  1. Does the tool use one file per agent or one combined file?
  2. Does it accept YAML frontmatter?
  3. Does it need the full canonical file or only the Markdown body?
  4. Is the installation scope user-wide or project-specific?
  5. Can an installation overwrite an existing user-maintained file?

Validate Generated Output

Successful execution does not guarantee valid output. Add checks for required files and empty content:

validate_output_file() {
  local file="$1"

  if [[ ! -f "$file" ]]; then
    printf 'ERROR: Expected output does not exist: %s\n' "$file" >&2
    return 1
  fi

  if [[ ! -s "$file" ]]; then
    printf 'ERROR: Generated output is empty: %s\n' "$file" >&2
    return 1
  fi
}

validate_output_file \
  "$OUT_DIR/cursor/.cursor/rules/agency-api-tester.mdc"

validate_output_file \
  "$OUT_DIR/aider/CONVENTIONS.md"
Enter fullscreen mode Exit fullscreen mode

You can also verify the number of generated files:

source_count="$(
  find engineering design testing marketing sales \
    -type f \
    -name '*.md' |
    wc -l
)"

cursor_count="$(
  find integrations/cursor/.cursor/rules \
    -type f \
    -name '*.mdc' |
    wc -l
)"

if [[ "$source_count" -ne "$cursor_count" ]]; then
  printf 'ERROR: Expected %s Cursor rules, found %s\n' \
    "$source_count" \
    "$cursor_count" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Add a Dry-Run Mode

A dry run lets you inspect destination paths without writing files.

Add an option parser:

DRY_RUN=false

if [[ "${1:-}" == "--dry-run" ]]; then
  DRY_RUN=true
fi
Enter fullscreen mode Exit fullscreen mode

Create a write helper:

write_cursor_file() {
  local agent_file="$1"
  local output="$2"

  if [[ "$DRY_RUN" == true ]]; then
    printf '[dry-run] Would write %s from %s\n' "$output" "$agent_file"
    return
  fi

  mkdir -p "$(dirname "$output")"

  {
    printf '%s\n' "---"
    printf 'description: Agency agent: %s\n' \
      "$(get_field "description" "$agent_file")"
    printf '%s\n' "---"
    get_body "$agent_file"
  } > "$output"
}
Enter fullscreen mode Exit fullscreen mode

Run it with:

scripts/convert.sh --dry-run
Enter fullscreen mode Exit fullscreen mode

Apply the same pattern to installation so you can see which existing files would be replaced.

Incremental Conversion

For a large repository, avoid regenerating unchanged agents. Record a hash for every source file:

#!/usr/bin/env bash

set -euo pipefail

CACHE_FILE="$REPO_ROOT/.conversion-cache"
TEMP_CACHE="${CACHE_FILE}.tmp"

declare -A PREVIOUS_HASHES

if [[ -f "$CACHE_FILE" ]]; then
  while IFS='=' read -r file hash; do
    PREVIOUS_HASHES["$file"]="$hash"
  done < "$CACHE_FILE"
fi

: > "$TEMP_CACHE"

for agent_file in "$REPO_ROOT"/engineering/*.md; do
  [[ -f "$agent_file" ]] || continue

  current_hash="$(md5sum "$agent_file" | cut -d' ' -f1)"
  previous_hash="${PREVIOUS_HASHES[$agent_file]:-}"

  if [[ "$current_hash" != "$previous_hash" ]]; then
    printf 'Changed: %s\n' "$agent_file"

    convert_cursor "$agent_file"
    convert_claude_code "$agent_file"
  else
    printf 'Unchanged: %s\n' "$agent_file"
  fi

  printf '%s=%s\n' "$agent_file" "$current_hash" >> "$TEMP_CACHE"
done

mv "$TEMP_CACHE" "$CACHE_FILE"
Enter fullscreen mode Exit fullscreen mode

This example requires Bash 4 or newer because it uses associative arrays.

For cross-platform scripts, remember that checksum commands differ:

  • Linux commonly provides md5sum.
  • macOS commonly provides md5.
  • Git repositories can use git hash-object.

Parallel Conversion

Independent per-agent outputs can be generated in parallel. Combined files such as CONVENTIONS.md and .windsurfrules should still be built serially to avoid concurrent writes.

With GNU Parallel:

#!/usr/bin/env bash

set -euo pipefail

export OUT_DIR
export -f get_field
export -f get_body
export -f to_kebab
export -f convert_cursor
export -f convert_claude_code

find "$REPO_ROOT" \
  -type f \
  -name '*.md' \
  -print0 |
  parallel \
    --null \
    --jobs 8 \
    --progress \
    '
      name="$(get_field "name" "{}")"
      printf "Converting: %s\n" "$name"
      convert_cursor "{}"
      convert_claude_code "{}"
    '

printf '%s\n' "Parallel conversion complete."
Enter fullscreen mode Exit fullscreen mode

Use null-delimited paths so filenames containing spaces are handled correctly.

Do not run these functions concurrently if they append to the same output:

convert_aider
convert_windsurf
Enter fullscreen mode Exit fullscreen mode

Build combined files in a separate serial phase after parallel per-agent conversion finishes.

Progress Tracking

For long serial conversions, print a simple progress bar:

mapfile -d '' agent_files < <(
  find "$REPO_ROOT" \
    -type f \
    -name '*.md' \
    -print0
)

total_files="${#agent_files[@]}"
current=0

for agent_file in "${agent_files[@]}"; do
  ((current += 1))

  percent=$((current * 100 / total_files))
  filled=$((percent / 5))
  empty=$((20 - filled))

  bar="$(printf '%*s' "$filled" '' | tr ' ' '#')"
  spaces="$(printf '%*s' "$empty" '')"
  name="$(get_field "name" "$agent_file")"

  printf '\r[%s%s] %s%% - %s' \
    "$bar" \
    "$spaces" \
    "$percent" \
    "$name"

  convert_cursor "$agent_file"
done

printf '\n'
Enter fullscreen mode Exit fullscreen mode

Security Considerations for Shared Agents

Agent files are instructions. Treat files from external sources as untrusted input, especially when they contain shell commands, network requests, or instructions to modify files.

Start with structural validation:

validate_agent() {
  local file="$1"
  local name
  local description

  name="$(get_field "name" "$file")"
  description="$(get_field "description" "$file")"

  if [[ -z "$name" ]]; then
    printf 'ERROR: Missing name field in %s\n' "$file" >&2
    return 1
  fi

  if [[ -z "$description" ]]; then
    printf 'WARNING: Missing description field in %s\n' "$file" >&2
  fi

  printf 'VALID: %s\n' "$name"
}
Enter fullscreen mode Exit fullscreen mode

You can add a heuristic scan for commands that require manual review:

scan_agent_body() {
  local file="$1"
  local body

  body="$(get_body "$file")"

  if printf '%s\n' "$body" |
    grep -Eq 'rm[[:space:]]+-rf|curl|wget|eval|exec'; then
    printf 'REVIEW REQUIRED: command-like content in %s\n' "$file" >&2
    return 1
  fi
}
Enter fullscreen mode Exit fullscreen mode

This is not proof that a file is malicious. It is only a review gate and may produce false positives.

For agents from untrusted sources:

  • Review the complete source before conversion.
  • Run tests in a temporary directory or container.
  • Mount sensitive files as read-only or do not mount them.
  • Restrict network access where possible.
  • Avoid exposing credentials or production configuration.
  • Log filesystem and command activity for later review.

Troubleshooting

The script fails with bad substitution

Verify that the script runs under Bash:

head -1 scripts/convert.sh
bash --version
bash scripts/convert.sh
Enter fullscreen mode Exit fullscreen mode

The first line should be:

#!/usr/bin/env bash
Enter fullscreen mode Exit fullscreen mode

Do not run the script with sh scripts/convert.sh if it uses Bash-specific syntax.

If the script was edited on Windows, check for CRLF line endings:

sed -i 's/\r$//' scripts/convert.sh
Enter fullscreen mode Exit fullscreen mode

Frontmatter fields are empty

Check that:

  • The opening and closing delimiters are exactly ---.
  • Field names do not have leading indentation.
  • A space follows the colon.
  • The field appears inside the first frontmatter block.

Expected:

---
name: API Tester
description: Tests API behavior
---
Enter fullscreen mode Exit fullscreen mode

Test the parser directly:

scripts/parse-frontmatter.sh --demo testing/api-tester.md
Enter fullscreen mode Exit fullscreen mode

Slugs contain unexpected characters

Test edge cases:

to_kebab "API Tester"
to_kebab "Backend / Platform Architect"
to_kebab "QA -- Automation"
Enter fullscreen mode Exit fullscreen mode

Expected results:

api-tester
backend-platform-architect
qa-automation
Enter fullscreen mode Exit fullscreen mode

For non-ASCII names, transliterate before applying the regular expressions:

to_kebab() {
  printf '%s' "$1" |
    iconv -f utf-8 -t ascii//TRANSLIT |
    tr '[:upper:]' '[:lower:]' |
    sed 's/[^a-z0-9]/-/g' |
    sed 's/--*/-/g' |
    sed 's/^-//' |
    sed 's/-$//'
}
Enter fullscreen mode Exit fullscreen mode

Always reject an empty result:

slug="$(to_kebab "$name")"

if [[ -z "$slug" ]]; then
  printf 'ERROR: Empty slug for agent name: %s\n' "$name" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Cursor rules are not loading

Verify the generated path:

find .cursor/rules -type f -name '*.mdc' -print
Enter fullscreen mode Exit fullscreen mode

Inspect the frontmatter:

head -5 .cursor/rules/agency-api-tester.mdc
Enter fullscreen mode Exit fullscreen mode

It should look like:

---
description: Agency agent: Specialized in API testing
---
Enter fullscreen mode Exit fullscreen mode

Also check that:

  • The extension is .mdc.
  • The files are in .cursor/rules/.
  • The frontmatter has both delimiters.
  • Cursor has been restarted after installing the rules.

CONVENTIONS.md becomes too large

Options include:

  • Split agents by category.
  • Remove deprecated agents before conversion.
  • Add a generated table of contents.
  • Install only the agents needed by a project.

For example:

CONVENTIONS-engineering.md
CONVENTIONS-design.md
CONVENTIONS-testing.md
Enter fullscreen mode Exit fullscreen mode

If the target tool supports includes, create a small root file that references the category files.

Combined files contain duplicated agents

The initializer must overwrite the generated file before the converters append to it:

initialize_aider
initialize_windsurf
Enter fullscreen mode Exit fullscreen mode

Do not append to an output left over from a previous conversion.

What You Built

Component Purpose
get_field() Extract a value from YAML frontmatter
get_body() Remove frontmatter and return the Markdown body
to_kebab() Convert display names into safe filenames
validate_agent() Reject agents missing required metadata
convert_claude_code() Copy canonical Markdown files
convert_cursor() Generate .mdc rules with description frontmatter
convert_aider() Append agents to CONVENTIONS.md
convert_windsurf() Append agents to .windsurfrules
convert_antigravity() Create one SKILL.md directory per agent
convert_openclaw() Generate three files per agent
convert.sh Run all conversions from one entry point
install.sh Copy generated files to tool-specific paths

Next Steps

Improve the pipeline by adding:

  • --dry-run support
  • Schema validation for required frontmatter
  • Automated output tests
  • Incremental conversion based on file hashes
  • Null-delimited file discovery
  • Parallel conversion for independent outputs
  • Category-based installation
  • Backups before replacing project-level files

You can also add converters for:

  • VS Code extensions
  • JetBrains IDEs
  • Internal developer portals
  • Custom agent runners
  • CI-based agent distribution

The core pattern stays the same:

parse -> validate -> transform -> verify -> install
Enter fullscreen mode Exit fullscreen mode

One agent file. Ten IDEs. Two Bash scripts.

Write once, convert automatically, and install wherever your team works.

Key Takeaways

  • Use one Markdown file with YAML frontmatter as the canonical agent definition.
  • Parse simple metadata with awk, but use a real YAML parser for complex frontmatter.
  • Keep tool-specific behavior inside separate conversion functions.
  • Generate per-agent files in parallel only when they do not share an output.
  • Build combined files such as CONVENTIONS.md serially.
  • Validate generated files before installation.
  • Back up project-level files before replacing them.
  • Treat externally sourced agent instructions as untrusted content.

FAQ

What is convert.sh?

convert.sh reads canonical agent Markdown files, extracts YAML frontmatter and body content, and generates the format required by each target tool. The implementation uses awk for frontmatter parsing, sed for slug generation, and heredocs or grouped commands for output generation.

How does frontmatter parsing work in Bash?

get_field() counts --- delimiters, searches only inside the first frontmatter block, and removes the matching field prefix. get_body() prints content after the second delimiter.

Which IDEs and tools can this approach support?

The example covers Claude Code, Cursor, Aider, Windsurf, Antigravity, and OpenClaw. The same pattern can also support GitHub Copilot, Gemini CLI extensions, OpenCode, Qwen Code, and other tools with documented agent or rule formats.

How do I add another tool?

Create a convert_your_tool() function that:

  1. Receives an agent file.
  2. Extracts the required metadata.
  3. Generates a stable slug.
  4. Creates the destination directory.
  5. Writes the target format.
  6. Runs inside the main conversion loop.

Can conversions run in parallel?

Yes, when each conversion writes to a separate file. Claude Code and Cursor conversions are good candidates. Do not parallelize multiple functions that append to the same CONVENTIONS.md or .windsurfrules file without explicit synchronization.

How do I validate required frontmatter?

Check each field before conversion:

name="$(get_field "name" "$agent_file")"

if [[ -z "$name" ]]; then
  printf 'ERROR: Missing name field in %s\n' "$agent_file" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Run validation before creating output files.

What should happen if one agent cannot be converted?

For strict CI pipelines, use set -euo pipefail and stop immediately. For batch migrations, record the failure and continue:

if ! validate_agent "$agent_file"; then
  printf '%s\n' "$agent_file" >> conversion-failures.log
  continue
fi
Enter fullscreen mode Exit fullscreen mode

After conversion, fail the job if conversion-failures.log contains any entries.

Top comments (0)