Skill: donttal/multi-agent-skill-sync (version 1.2.1)

--- name: skill-sync description: One source of truth for local AI agent skills: audit Codex, Claude, OpenClaw, OpenCode, workspace skills, and shared roots; score hygiene, diff conflicting installs, deduplicate into one canonical source, and migrate the same layout across machines with restorable backups. homepage: https://github.com/LearnPrompt/skill-sync --- # Skill Sync ## When To Use Use this skill when the user wants to: - scan local skills across Codex, Claude, OpenClaw, OpenCode, workspace `./skills`, and shared roots - see which skills are `shared`, `duplicate`, `compatible`, `specific`, or `mixed` - choose one canonical source and replace duplicate copies with symlinks - preview or apply convergence onto one preferred root such as `~/.agents/skills` - export a portable layout manifest and import the same topology on another machine - restore a previous dedupe or import run Do not use this skill when the task is only about one host and no cross-host comparison or symlink management is needed. ## Fast Path Scan the machine and get the hygiene score plus recommended actions: ```bash python3 scripts/skill_sync.py ``` List only shared and host-specific skills: ```bash python3 scripts/skill_sync.py --status shared,specific --list-names ``` Inspect one conflicting portable skill: ```bash python3 scripts/skill_sync.py --diff rapid-ocr ``` Preview safe dedupe: ```bash python3 scripts/skill_sync.py --dedupe --strategy strict ``` Preview a single-root convergence plan: ```bash python3 scripts/skill_sync.py --adopt-root agents ``` Apply convergence with backups: ```bash python3 scripts/skill_sync.py --adopt-root agents --apply ``` Restore the latest run: ```bash python3 scripts/skill_sync.py --restore latest python3 scripts/skill_sync.py --restore latest --apply ``` ## Workflow 1. Scan first. Run `python3 scripts/skill_sync.py` and read the hygiene score and recommended actions. 2. Review risky groups before mutation. Use `--status compatible --list-names`, `--status mixed --list-names`, and `--diff <skill>`. 3. Preview convergence. Use `--dedupe --strategy strict` for identical groups, or `--adopt-root <platform>` to converge around one root. 4. Apply only when the plan looks right. Add `--apply` to execute symlink creation or replacement. 5. Export or import machine layouts when needed. Use `--export-manifest` and `--import-manifest` for cross-machine reuse. 6. Restore from backup if needed. Use `--restore <run-id|latest>` and add `--apply` to roll back. ## Safety Rules - The script always scans and reports before mutation. - It only auto-links portable directory-based skills that contain `SKILL.md`. - It never overwrites an existing destination without first moving it into `~/.skill-sync/backups/<run-id>/originals/...`. - `--strategy strict` only dedupes identical portable skills. - `--strategy prefer-latest` and `--strategy trust-high` may select the newest portable copy when content differs. - `--restore latest --apply` replays the last backup manifest in reverse. ## Roots Scanned - `<current-workdir>/skills` - `~/.codex/skills` - `~/.agents/skills` - `~/.claude/skills` - `~/.claude/skills/anthropic-skills/skills` - `~/.config/opencode/skills` - `~/.openclaw/skills` - `~/.openclaw/extensions/*/skills` ## Status Model - `shared`: the same real directory is visible from multiple hosts, usually through symlinks - `duplicate`: multiple hosts have the same portable skill content, but not via the same real path yet - `compatible`: multiple hosts have portable `SKILL.md` skills with the same name, but different content - `specific`: the skill appears on only one host - `mixed`: the same skill name exists on multiple hosts, but with different formats or incompatible content If you need the detection details or compatibility notes, read [references/compatibility.md](./references/compatibility.md).

path: README.md
content: # Skill Sync

One source of truth for local AI agent skills.

`skill-sync` turns a messy multi-agent setup into a maintainable local skill system:

- scan skills across Codex, Claude, OpenClaw, OpenCode, workspace `./skills`, and shared agent roots
- score the hygiene of your current setup
- show shared, duplicate, compatible, and host-specific skills
- pick a canonical source automatically
- replace duplicate copies with symlinks
- back up every replaced install so you can restore it later
- export a portable layout manifest and recreate that topology on another machine

## Who This Is For

`skill-sync` is built for people who:

- use more than one AI coding agent
- install lots of local skills
- keep a shared skill library in `~/.agents/skills`
- move between machines or rebuild environments often
- want one canonical skill source instead of version drift everywhere

## The Problem

In a real machine, the same skill often appears in multiple places:

- `~/.codex/skills`
- `~/.claude/skills`
- `~/.openclaw/skills`
- `~/.config/opencode/skills`
- `~/.agents/skills`
- `<workspace>/skills`

That creates drift:

- one copy gets updated, another stays stale
- the same skill gets installed repeatedly
- nobody remembers which copy is the real source of truth

## The Product Promise

`skill-sync` gives you four things that basic skill managers usually do not:

1. Cross-host discovery  
It scans all the common local roots at once instead of showing one host in isolation.

2. Canonical source selection  
It decides which copy should win based on strategy, timestamps, shared roots, and your preferred source order.

3. Safe convergence  
It can turn duplicates into symlinks and centralize ownership without destructive blind replacement.

4. Reversible operations  
Every dedupe run writes a restorable backup manifest under `~/.skill-sync/backups`.

## What Makes It Different

Compared with generic local skill managers, `skill-sync` is specifically about:

- cross-agent skill hygiene
- canonical source adoption
- symlink-based dedupe
- compatible skill diffing
- reversible local convergence

## Supported Skill Roots

- `<current-workdir>/skills`
- `~/.codex/skills`
- `~/.agents/skills`
- `~/.claude/skills`
- `~/.claude/skills/anthropic-skills/skills`
- `~/.config/opencode/skills`
- `~/.openclaw/skills`
- `~/.openclaw/extensions/*/skills`

## Quick Start

Install into your main hosts:

```bash
git clone https://github.com/LearnPrompt/skill-sync.git
cd skill-sync
./install.sh --codex --claude --openclaw --agents
```

Scan everything:

```bash
python3 scripts/skill_sync.py
```

Export the current topology as a migration manifest:

```bash
python3 scripts/skill_sync.py \
  --adopt-root agents \
  --export-manifest .skill-sync/agent-layout.json
```

Preview that layout on another machine:

```bash
python3 scripts/skill_sync.py \
  --import-manifest .skill-sync/agent-layout.json
```

Apply it with backups:

```bash
python3 scripts/skill_sync.py \
  --import-manifest .skill-sync/agent-layout.json \
  --apply
```

Show only shared and host-specific skills:

```bash
python3 scripts/skill_sync.py --status shared,specific --list-names
```

Inspect a compatibility conflict:

```bash
python3 scripts/skill_sync.py --diff rapid-ocr
```

Preview safe dedupe:

```bash
python3 scripts/skill_sync.py --dedupe --strategy strict
```

Preview a full convergence onto the shared agent root:

```bash
python3 scripts/skill_sync.py --adopt-root agents
```

Apply that convergence:

```bash
python3 scripts/skill_sync.py --adopt-root agents --apply
```

Restore the latest run:

```bash
python3 scripts/skill_sync.py --restore latest
python3 scripts/skill_sync.py --restore latest --apply
```

## Example Output

```text
Discovered 99 unique skills from 183 installs.
Hygiene score: 46/100 (risky) | shared_ratio=38.4% | review=2 | duplicates=20

RECOMMENDED ACTIONS
- [high] Review 2 compatible skills before dedupe
- [medium] Deduplicate 20 identical multi-host skills
- [low] Preview a single-root convergence plan
```

The point is not just to list skills. The point is to tell you what to do next.

## Validation

Run the fast local checks:

```bash
python3 -m py_compile scripts/skill_sync.py
python3 -m unittest discover -s tests -q
```

## Status Model

- `shared`: multiple hosts already point to the same real path
- `duplicate`: portable installs match exactly but live at different real paths
- `compatible`: portable installs share a name but differ in content
- `specific`: found on only one host
- `mixed`: same name exists with incompatible formats or host-specific layouts

## Strategy Model

- `strict`: only dedupe identical portable skills
- `prefer-latest`: keep the newest portable copy when content differs
- `trust-high`: same canonical logic as `prefer-latest`, but allows more aggressive replacement of scanned roots

## Superpowers

### Hygiene Score

Every scan computes a rough operational score so you can tell whether your local skill ecosystem is clean or drifting.

### Recommended Actions

The report suggests next steps instead of dumping raw data only.

### File-Level Diff

Use `--diff <skill>` to compare portable installs against the selected canonical source and see which files changed, were added, or removed.

### Root Adoption

Use `--adopt-root agents` or another root to preview or apply a convergence plan around one canonical host.

### Cross-Machine Migration

Use `--export-manifest` to save the desired symlink topology and `--import-manifest` to recreate that topology elsewhere.

The manifest records:

- which skills are portable enough to converge
- which host should act as canonical source
- which primary hosts should expose each skill

The manifest does not copy skill payloads themselves. On the target machine, the canonical source still needs to exist locally.

## About `~/.agents/skills`

If you already use `~/.agents/skills` as a shared skill library, that works especially well with `skill-sync`.

By default:

- it participates in discovery
- it often becomes the canonical source because it is first in the default source order

That is usually desirable. If you want a different preference order:

```bash
python3 scripts/skill_sync.py --source-order workspace,codex,claude,agents,opencode,openclaw
```

## Backup Model

Applied runs are written to:

```text
~/.skill-sync/backups/<run-id>/
```

Each run stores:

- `manifest.json`
- `originals/...`
- `latest`

This makes dedupe reversible. A real directory is moved to backup before a symlink replaces it.

## Install Script

`install.sh` supports:

- `--codex`
- `--agents`
- `--claude`
- `--opencode`
- `--openclaw`
- `--all`
- `--copy`
- `--force`

Examples:

```bash
./install.sh --all
./install.sh --codex --claude --force
./install.sh --openclaw --copy
```

## Project Layout

```text
.
├── SKILL.md
├── README.md
├── install.sh
├── agents/openai.yaml
├── references/compatibility.md
└── scripts/skill_sync.py
```

## License

MIT


path: agents/openai.yaml
content: interface:
  display_name: "Skill Sync"
  short_description: "One source of truth for local AI agent skills"
  default_prompt: "Use $skill-sync when the user wants to audit skills across Codex, Claude, OpenClaw, OpenCode, workspace skills, and shared roots, then score hygiene, review shared or conflicting installs, and preview or apply a safe canonical-source convergence, migration, or restore plan."


path: install.sh
content: #!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
skill_name="skill-sync"

declare -a targets=()
mode="link"
force="false"

usage() {
  cat <<'EOF'
Install skill-sync into local AI agent skill directories.

Usage:
  ./install.sh [options]

Targets:
  --codex       Install into ~/.codex/skills
  --agents      Install into ~/.agents/skills
  --claude      Install into ~/.claude/skills
  --opencode    Install into ~/.config/opencode/skills
  --openclaw    Install into ~/.openclaw/skills
  --all         Install into all primary targets above

Options:
  --copy        Copy files instead of creating a symlink
  --force       Replace an existing destination
  --help        Show this help

Examples:
  ./install.sh --codex
  ./install.sh --codex --claude --openclaw
  ./install.sh --all
  ./install.sh --codex --copy
EOF
}

append_target() {
  local name="$1"
  for existing in "${targets[@]:-}"; do
    if [[ "$existing" == "$name" ]]; then
      return 0
    fi
  done
  targets+=("$name")
}

target_root() {
  case "$1" in
    codex) printf '%s\n' "${HOME}/.codex/skills" ;;
    agents) printf '%s\n' "${HOME}/.agents/skills" ;;
    claude) printf '%s\n' "${HOME}/.claude/skills" ;;
    opencode) printf '%s\n' "${HOME}/.config/opencode/skills" ;;
    openclaw) printf '%s\n' "${HOME}/.openclaw/skills" ;;
    *)
      printf 'Unknown target: %s\n' "$1" >&2
      exit 1
      ;;
  esac
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --codex) append_target "codex" ;;
    --agents) append_target "agents" ;;
    --claude) append_target "claude" ;;
    --opencode) append_target "opencode" ;;
    --openclaw) append_target "openclaw" ;;
    --all)
      append_target "codex"
      append_target "agents"
      append_target "claude"
      append_target "opencode"
      append_target "openclaw"
      ;;
    --copy) mode="copy" ;;
    --force) force="true" ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      printf 'Unknown option: %s\n\n' "$1" >&2
      usage >&2
      exit 1
      ;;
  esac
  shift
done

if [[ ${#targets[@]} -eq 0 ]]; then
  printf 'No install target selected.\n\n' >&2
  usage >&2
  exit 1
fi

for target in "${targets[@]}"; do
  root="$(target_root "$target")"
  destination="${root}/${skill_name}"

  mkdir -p "$root"

  if [[ -L "$destination" || -e "$destination" ]]; then
    if [[ "$force" != "true" ]]; then
      printf 'Skip %s: %s already exists (use --force to replace)\n' "$target" "$destination"
      continue
    fi
    rm -rf "$destination"
  fi

  if [[ "$mode" == "copy" ]]; then
    cp -R "$repo_root" "$destination"
    printf 'Copied to %s: %s\n' "$target" "$destination"
  else
    ln -s "$repo_root" "$destination"
    printf 'Linked to %s: %s -> %s\n' "$target" "$destination" "$repo_root"
  fi
done


path: references/compatibility.md
content: # Compatibility Model

`skill-sync` uses a simple portability rule:

- A skill directory with `SKILL.md` is treated as portable and eligible for symlink-based sharing.
- A standalone `*.skill` file is treated as Claude-specific.
- A directory without `SKILL.md` is treated as host-specific unless you manually standardize it later.

This heuristic is intentional. It keeps the default sync path safe and predictable.

## Current Host Roots

- `codex`: `~/.codex/skills`
- `agents`: `~/.agents/skills`
- `claude`: `~/.claude/skills`
- `claude-vendor`: `~/.claude/skills/anthropic-skills/skills`
- `opencode`: `~/.config/opencode/skills`
- `openclaw`: `~/.openclaw/skills`
- `openclaw-plugin`: `~/.openclaw/extensions/*/skills`

Only primary roots are used as symlink destinations:

- `codex`
- `agents`
- `claude`
- `opencode`
- `openclaw`

Nested vendor/plugin roots are scanned for discovery, but they are not used as sync targets.

## Deduplication Strategies

- `strict`: only dedupe groups whose full portable content hash matches
- `prefer-latest`: dedupe any all-portable `SKILL.md` group and keep the newest real path
- `trust-high`: same canonical source rule as `prefer-latest`, but it may also replace scanned vendor/plugin installs

## Backup And Restore

Applied runs are stored under:

- `~/.skill-sync/backups/<run-id>/manifest.json`
- `~/.skill-sync/backups/<run-id>/originals/...`
- `~/.skill-sync/backups/latest`

Restore flow:

1. Preview with `--restore latest`
2. Apply with `--restore latest --apply`

The manifest records:

- action type
- original path
- symlink target
- backup payload path when a duplicate install was replaced


path: tests/test_skill_sync_cli.py
content: from __future__ import annotations

import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = REPO_ROOT / "scripts" / "skill_sync.py"


def write_skill(root: Path, name: str, payload: str) -> Path:
    skill_dir = root / name
    skill_dir.mkdir(parents=True, exist_ok=True)
    (skill_dir / "SKILL.md").write_text(
        f"---\nname: {name}\ndescription: fixture\n---\n\n# {name}\n",
        encoding="utf-8",
    )
    (skill_dir / "payload.txt").write_text(payload, encoding="utf-8")
    return skill_dir


class SkillSyncCliTests(unittest.TestCase):
    def run_cli(self, machine_root: Path, *args: str) -> subprocess.CompletedProcess[str]:
        env = os.environ.copy()
        env.update(
            {
                "HOME": str(machine_root),
                "SKILL_SYNC_AGENTS_ROOT": str(machine_root / "agents"),
                "SKILL_SYNC_CODEX_ROOT": str(machine_root / "codex"),
                "SKILL_SYNC_CLAUDE_ROOT": str(machine_root / "claude"),
                "SKILL_SYNC_OPENCODE_ROOT": str(machine_root / "opencode"),
                "SKILL_SYNC_OPENCLAW_ROOT": str(machine_root / "openclaw"),
                "SKILL_SYNC_BACKUP_ROOT": str(machine_root / "backups"),
            }
        )
        return subprocess.run(
            [sys.executable, str(SCRIPT_PATH), *args],
            cwd=str(REPO_ROOT),
            env=env,
            text=True,
            capture_output=True,
            check=True,
        )

    def test_export_import_apply_restore_round_trip(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_root = Path(temp_dir)
            machine_a = temp_root / "machine-a"
            machine_b = temp_root / "machine-b"
            manifest_path = temp_root / "agent-layout.json"

            for machine_root in (machine_a, machine_b):
                for root_name in ("agents", "codex", "backups"):
                    (machine_root / root_name).mkdir(parents=True, exist_ok=True)

            write_skill(machine_a / "agents", "demo-skill", "canonical\n")
            write_skill(machine_a / "codex", "demo-skill", "canonical\n")

            export_result = self.run_cli(
                machine_a,
                "--adopt-root",
                "agents",
                "--export-manifest",
                str(manifest_path),
                "--format",
                "json",
            )
            exported = json.loads(export_result.stdout)
            self.assertEqual(exported["kind"], "skill-sync-layout")
            self.assertEqual(exported["summary"]["importable_skills"], 1)
            self.assertEqual(exported["skills"][0]["canonical"]["platform"], "agents")
            self.assertEqual(
                exported["skills"][0]["desired_platforms"],
                ["agents", "codex"],
            )

            write_skill(machine_b / "agents", "demo-skill", "canonical\n")
            write_skill(machine_b / "codex", "demo-skill", "legacy-copy\n")

            preview_result = self.run_cli(
                machine_b,
                "--import-manifest",
                str(manifest_path),
                "--format",
                "json",
            )
            preview = json.loads(preview_result.stdout)
            self.assertEqual(len(preview["operations"]), 1)
            self.assertEqual(preview["operations"][0]["action"], "replace_with_symlink")

            apply_result = self.run_cli(
                machine_b,
                "--import-manifest",
                str(manifest_path),
                "--apply",
                "--format",
                "json",
            )
            applied = json.loads(apply_result.stdout)
            run_id = applied["run"]["run_id"]
            codex_skill = machine_b / "codex" / "demo-skill"
            self.assertTrue(codex_skill.is_symlink())
            self.assertEqual(
                codex_skill.resolve(),
                (machine_b / "agents" / "demo-skill").resolve(),
            )
            self.assertTrue((machine_b / "backups" / run_id / "manifest.json").is_file())
            self.assertTrue((machine_b / "backups" / "latest").is_symlink())

            restore_result = self.run_cli(
                machine_b,
                "--restore",
                "latest",
                "--apply",
                "--format",
                "json",
            )
            restored = json.loads(restore_result.stdout)
            self.assertEqual(restored["run_id"], run_id)
            self.assertFalse(codex_skill.is_symlink())
            self.assertEqual((codex_skill / "payload.txt").read_text(encoding="utf-8"), "legacy-copy\n")


if __name__ == "__main__":
    unittest.main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)