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.
TL;DR
The conversion pipeline has three stages:
- Parse YAML frontmatter with
get_field(),get_body(), andto_kebab(). - Transform the agent into tool-specific formats with
convert.sh. - 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
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:
- YAML frontmatter containing metadata.
- 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
The conversion scripts need to:
- Extract fields such as
nameanddescription. - 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/
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
Make the script executable and test it:
chmod +x scripts/parse-frontmatter.sh
scripts/parse-frontmatter.sh \
--demo \
engineering/backend-architect.md
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...
Understand the parser's limits
This parser intentionally handles simple, flat frontmatter such as:
name: API Tester
description: Tests API behavior and contracts
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")"
}
The generated structure is:
integrations/
โโโ claude-code/
โโโ api-tester.md
โโโ backend-architect.md
During installation, these files are copied to:
~/.claude/agents/
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"
}
Given this input:
---
name: API Tester
description: Specialized in API testing
---
# API Tester Agent
Validate API contracts, error responses, and edge cases.
The converter generates:
---
description: Agency agent: Specialized in API testing
---
# API Tester Agent
Validate API contracts, error responses, and edge cases.
The output path is:
integrations/cursor/.cursor/rules/agency-api-tester.mdc
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")"
}
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
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")"
}
The generated file is installed as:
./.windsurfrules
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"
}
The output structure is:
integrations/
โโโ antigravity/
โโโ skills/
โโโ agency-api-tester/
โโโ SKILL.md
Step 7: Convert Agents for OpenClaw
OpenClaw uses three files per agent:
-
SOUL.mdfor the full agent definition -
AGENTS.mdfor capabilities -
IDENTITY.mdfor 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"
}
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 "$@"
Run the conversion:
chmod +x scripts/convert.sh
scripts/convert.sh
Inspect the generated files before installing them:
find integrations -type f -print
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
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
Make it executable:
chmod +x scripts/install.sh
Install one integration:
scripts/install.sh cursor
Or install all supported integrations:
scripts/install.sh all
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"
}
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)
When implementing a new converter, answer these questions first:
- Does the tool use one file per agent or one combined file?
- Does it accept YAML frontmatter?
- Does it need the full canonical file or only the Markdown body?
- Is the installation scope user-wide or project-specific?
- 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"
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
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
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"
}
Run it with:
scripts/convert.sh --dry-run
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"
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."
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
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'
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"
}
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
}
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
The first line should be:
#!/usr/bin/env bash
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
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
---
Test the parser directly:
scripts/parse-frontmatter.sh --demo testing/api-tester.md
Slugs contain unexpected characters
Test edge cases:
to_kebab "API Tester"
to_kebab "Backend / Platform Architect"
to_kebab "QA -- Automation"
Expected results:
api-tester
backend-platform-architect
qa-automation
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/-$//'
}
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
Cursor rules are not loading
Verify the generated path:
find .cursor/rules -type f -name '*.mdc' -print
Inspect the frontmatter:
head -5 .cursor/rules/agency-api-tester.mdc
It should look like:
---
description: Agency agent: Specialized in API testing
---
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
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
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-runsupport - 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
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.mdserially. - 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:
- Receives an agent file.
- Extracts the required metadata.
- Generates a stable slug.
- Creates the destination directory.
- Writes the target format.
- 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
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
After conversion, fail the job if conversion-failures.log contains any entries.
Top comments (0)