Skip to content

Instantly share code, notes, and snippets.

@cameroncooke
Last active January 22, 2026 05:07
Show Gist options
  • Select an option

  • Save cameroncooke/5f40ea2f51697c1b2806174e7c2232d5 to your computer and use it in GitHub Desktop.

Select an option

Save cameroncooke/5f40ea2f51697c1b2806174e7c2232d5 to your computer and use it in GitHub Desktop.
Sync skills
#!/usr/bin/env bash
set -euo pipefail
source_dirs=(
"/Users/someone/code/some-skills"
"/Users/someone/code/sentry_skills"
"/Users/someone/code/my-skills"
)
codex_dest="$HOME/.codex/skills/public"
claude_skills_dest="$HOME/.claude/skills"
codex_union="$(mktemp -d)"
claude_union="$(mktemp -d)"
color_reset=$'\033[0m'
color_dim=$'\033[2m'
color_bold=$'\033[1m'
color_cyan=$'\033[36m'
color_green=$'\033[32m'
color_yellow=$'\033[33m'
color_purple=$'\033[35m'
term_width=$(tput cols 2>/dev/null || echo 60)
box_width=$((term_width > 70 ? 60 : term_width - 10))
print_header() {
local text="$1"
local pad=$(( (box_width - ${#text} - 2) / 2 ))
local pad_r=$(( box_width - ${#text} - 2 - pad ))
printf "\n${color_cyan}${color_bold}"
printf "╭%*s╮\n" "$box_width" "" | tr ' ' ''
printf "│%*s %s %*s│\n" "$pad" "" "$text" "$pad_r" ""
printf "╰%*s╯\n" "$box_width" "" | tr ' ' ''
printf "${color_reset}"
}
log_section() {
local icon="$1"
local title="$2"
printf "\n${color_purple}${color_bold} %s %s${color_reset}\n" "$icon" "$title"
printf "${color_dim} %*s${color_reset}\n" "${#title}" "" | tr ' ' ''
}
log_step() {
printf " ${color_cyan}${color_reset} %s" "$1"
}
log_ok() {
printf "\r ${color_green}${color_reset} %s\n" "$1"
}
trap 'rm -rf "$codex_union" "$claude_union"' EXIT
total_plugins=0
is_marketplace() {
[[ -f "$1/.claude-plugin/marketplace.json" ]]
}
count_dirs() {
local path="$1"
find "$path" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' '
}
get_orphaned_dirs() {
local source="$1"
local dest="$2"
local orphans=()
if [[ ! -d "$dest" ]]; then
return
fi
shopt -s nullglob
for dest_dir in "$dest"/*/; do
dir_name=$(basename "$dest_dir")
if [[ ! -d "$source/$dir_name" ]]; then
orphans+=("$dir_name")
fi
done
shopt -u nullglob
printf '%s\n' "${orphans[@]}"
}
prompt_deploy_mode() {
local codex_orphans claude_orphans
codex_orphans=$(get_orphaned_dirs "$codex_union" "$codex_dest")
claude_orphans=$(get_orphaned_dirs "$claude_union" "$claude_skills_dest")
if [[ -z "$codex_orphans" && -z "$claude_orphans" ]]; then
deploy_mode="replace"
return
fi
printf "\n${color_yellow}${color_bold} ⚠ Existing skills detected${color_reset}\n"
printf "${color_dim} The following skills exist at the destination but not in sources:${color_reset}\n\n"
if [[ -n "$codex_orphans" ]]; then
printf " ${color_bold}Codex:${color_reset}\n"
while IFS= read -r orphan; do
[[ -n "$orphan" ]] && printf " ${color_dim}${color_reset} %s\n" "$orphan"
done <<< "$codex_orphans"
echo
fi
if [[ -n "$claude_orphans" ]]; then
printf " ${color_bold}Claude:${color_reset}\n"
while IFS= read -r orphan; do
[[ -n "$orphan" ]] && printf " ${color_dim}${color_reset} %s\n" "$orphan"
done <<< "$claude_orphans"
echo
fi
printf " ${color_cyan}[R]${color_reset} Replace all ${color_dim}(remove unlisted skills)${color_reset}\n"
printf " ${color_cyan}[M]${color_reset} Merge ${color_dim}(keep existing, add new)${color_reset}\n"
printf " ${color_cyan}[C]${color_reset} Cancel\n\n"
while true; do
printf " Choice ${color_dim}[R/M/C]${color_reset}: "
read -r choice
case "${choice,,}" in
r|replace)
deploy_mode="replace"
break
;;
m|merge)
deploy_mode="merge"
break
;;
c|cancel)
printf "\n${color_yellow} ✗ Cancelled${color_reset}\n\n"
exit 0
;;
*)
printf " ${color_yellow}Invalid choice. Please enter R, M, or C.${color_reset}\n"
;;
esac
done
}
sync_skill_dirs() {
local skills_path="$1"
local dest="$2"
shopt -s nullglob
local dirs=("$skills_path"/*/)
shopt -u nullglob
for dir in "${dirs[@]}"; do
if [[ -f "${dir}SKILL.md" ]]; then
rsync -a --exclude ".git" "${dir%/}" "$dest/"
fi
done
}
print_header "Skills Updater"
log_section "📥" "Fetching Updates"
required_tools=(git rsync jq claude)
missing_tools=()
for tool in "${required_tools[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
fi
done
if ((${#missing_tools[@]} > 0)); then
printf " ${color_yellow}${color_reset} Missing required tools: %s\n" "${missing_tools[*]}" >&2
exit 1
fi
for source_dir in "${source_dirs[@]}"; do
if [[ ! -d "$source_dir" ]]; then
printf " ${color_yellow}${color_reset} Missing: %s ${color_dim}(skipping)${color_reset}\n" "$source_dir" >&2
continue
fi
repo_root="$(git -C "$source_dir" rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$repo_root" || ! -d "$repo_root/.git" ]]; then
printf " ${color_yellow}${color_reset} No git repo: %s\n" "$source_dir" >&2
exit 1
fi
repo_name=$(basename "$repo_root")
log_step "$repo_name"
output=$(git -C "$repo_root" pull --ff-only 2>&1)
if [[ "$output" == *"Already up to date"* ]]; then
printf "\r ${color_green}${color_reset} %s ${color_dim}(up to date)${color_reset}\n" "$repo_name"
else
printf "\r ${color_green}${color_reset} %s ${color_green}(updated)${color_reset}\n" "$repo_name"
fi
done
log_section "📦" "Installing Skills"
for source_dir in "${source_dirs[@]}"; do
dir_name=$(basename "$source_dir")
if [[ ! -d "$source_dir" ]]; then
printf "\n ${color_yellow}${color_reset} Missing: %s ${color_dim}(skipping)${color_reset}\n" "$source_dir"
continue
fi
if is_marketplace "$source_dir"; then
printf "\n ${color_cyan}${color_reset} ${color_bold}%s${color_reset} ${color_dim}(plugin source)${color_reset}\n" "$dir_name"
while IFS= read -r plugin_name; do
log_step "Installing $plugin_name"
claude plugin install --plugin-dir="$source_dir" "$plugin_name" > /dev/null 2>&1
log_ok "Plugin → Claude ${color_dim}($plugin_name)${color_reset}"
total_plugins=$((total_plugins + 1))
plugin_source=$(jq -r --arg name "$plugin_name" '.plugins[] | select(.name == $name) | .source' "$source_dir/.claude-plugin/marketplace.json")
plugin_path="$source_dir/$plugin_source"
if [[ -d "$plugin_path/skills" ]]; then
skill_count=$(find "$plugin_path/skills" -mindepth 1 -maxdepth 1 -type d -exec test -f '{}/SKILL.md' \; -print 2>/dev/null | wc -l | tr -d ' ')
sync_skill_dirs "$plugin_path/skills" "$codex_union"
[[ "$skill_count" -gt 0 ]] && printf " ${color_green}${color_reset} Skills → Codex ${color_dim}(%s)${color_reset}\n" "$skill_count"
fi
if [[ -d "$plugin_path/agents" ]]; then
for agent_file in "$plugin_path/agents"/*.md; do
[[ -f "$agent_file" ]] || continue
agent_name=$(basename "$agent_file" .md)
[[ "$agent_name" == "README" ]] && continue
mkdir -p "$codex_union/$agent_name"
sed -n '/^---$/,$p' "$agent_file" > "$codex_union/$agent_name/SKILL.md"
printf " ${color_green}${color_reset} Agent → Codex ${color_dim}(%s)${color_reset}\n" "$agent_name"
done
fi
done < <(jq -r '.plugins[].name' "$source_dir/.claude-plugin/marketplace.json")
else
skill_count=$(find "$source_dir" -mindepth 1 -maxdepth 1 -type d -exec test -f '{}/SKILL.md' \; -print 2>/dev/null | wc -l | tr -d ' ')
printf "\n ${color_cyan}${color_reset} ${color_bold}%s${color_reset} ${color_dim}(%s skills)${color_reset}\n" "$dir_name" "$skill_count"
sync_skill_dirs "$source_dir" "$codex_union"
printf " ${color_green}${color_reset} Skills → Codex\n"
sync_skill_dirs "$source_dir" "$claude_union"
printf " ${color_green}${color_reset} Skills → Claude\n"
fi
done
deploy_mode=""
prompt_deploy_mode
log_section "🚀" "Deploying"
deploy_to() {
local src="$1" dest="$2" name="$3"
local rsync_opts=(-a)
[[ "$deploy_mode" == "replace" ]] && rsync_opts+=(--delete)
mkdir -p "$dest"
rsync "${rsync_opts[@]}" "$src/" "$dest/"
printf " ${color_green}${color_reset} %s: %s ${color_dim}(%s)${color_reset}\n" "$name" "$dest" "$deploy_mode"
}
deploy_to "$codex_union" "$codex_dest" "Codex"
deploy_to "$claude_union" "$claude_skills_dest" "Claude"
codex_count=$(count_dirs "$codex_union")
claude_count=$(count_dirs "$claude_union")
printf "\n${color_green}${color_bold} ✓ All done!${color_reset}\n"
printf "${color_dim} Codex: %s skills │ Claude: %s skills, %s plugins${color_reset}\n\n" "$codex_count" "$claude_count" "$total_plugins"
@cameroncooke
Copy link
Author

Skills Updater

Keep Codex and Claude Code skills in sync with multiple sources. The script pulls your sources, builds clean unified skill sets, installs Claude plugins when needed, and makes plugin agents available to Codex as skills.

What it does

  • Pulls the latest changes from each configured source repo.
  • Builds a Codex-ready skill set and a Claude-ready skill set.
  • Installs Claude plugins directly instead of copying their skills into Claude.
  • Extracts plugin skills and agents, converts agents into Codex skills, and copies them into Codex.

Strategies

  • Union build: sources are merged into temporary “union” directories before deploying to Codex and Claude. This avoids partial updates.
  • Plugin-aware flow: plugin repos are treated differently from plain skill repos, so Claude stays plugin-first while Codex can still use plugin content.
  • Deploy modes: if destination skills don’t exist in sources, you can choose merge (keep extras) or replace (remove unlisted).

Configure your sources

Edit the source list at the top of the script:

source_dirs=(
  "/path/to/your/skills-repo"
  "/path/to/your/claude-plugin-repo"
)

Usage

./update_skills.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment