Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</p>

<p align="center">
<strong>One source of truth for AI CLI skills, agents, rules, commands & more. Sync everywhere with one command — from personal to organization-wide.</strong><br>
<strong>One source of truth for AI CLI skills, agents, native plugins, standalone hooks, rules, commands & more. Sync everywhere with one command — from personal to organization-wide.</strong><br>
Codex, Claude Code, OpenClaw, OpenCode & 60+ more.
</p>

Expand Down Expand Up @@ -51,7 +51,7 @@ skillshare fixes this:

- **One source, every agent** — sync to Claude, Cursor, Codex & 60+ more with `skillshare sync`
- **Agent management** — sync custom agents alongside skills to agent-capable targets
- **More than skills** — manage rules, commands, prompts & any file-based resource with [extras](https://skillshare.runkids.cc/docs/reference/targets/configuration#extras)
- **More than skills** — manage agents, native plugins, standalone hooks, and file-based resources with [extras](https://skillshare.runkids.cc/docs/reference/targets/configuration#extras)
- **Install from anywhere** — GitHub, GitLab, Bitbucket, Azure DevOps, or any self-hosted Git
- **Built-in security** — audit skills for prompt injection and data exfiltration before use
- **Team-ready** — project skills in `.skillshare/`, org-wide skills via tracked repos
Expand All @@ -71,6 +71,8 @@ skillshare fixes this:
│ ~/.config/skillshare/skills/ ← skills (SKILL.md) │
│ ~/.config/skillshare/agents/ ← agents │
│ ~/.config/skillshare/extras/ ← rules, commands, etc. │
│ ~/.config/skillshare/plugins/ ← native plugin bundles │
│ ~/.config/skillshare/hooks/ ← standalone hook bundles │
└─────────────────────────────────────────────────────────────┘
│ sync
┌───────────────┼───────────────┐
Expand All @@ -80,10 +82,21 @@ skillshare fixes this:
└───────────┘ └───────────┘ └───────────┘
```

| Platform | Skills Source | Agents Source | Extras Source | Link Type |
|----------|---------------|---------------|---------------|-----------|
| macOS/Linux | `~/.config/skillshare/skills/` | `~/.config/skillshare/agents/` | `~/.config/skillshare/extras/` | Symlinks |
| Windows | `%AppData%\skillshare\skills\` | `%AppData%\skillshare\agents\` | `%AppData%\skillshare\extras\` | NTFS Junctions (no admin required) |
| Platform | Skills Source | Agents Source | Plugins Source | Hooks Source | Extras Source | Link Type |
|----------|---------------|---------------|----------------|--------------|---------------|-----------|
| macOS/Linux | `~/.config/skillshare/skills/` | `~/.config/skillshare/agents/` | `~/.config/skillshare/plugins/` | `~/.config/skillshare/hooks/` | `~/.config/skillshare/extras/` | Symlinks |
| Windows | `%AppData%\skillshare\skills\` | `%AppData%\skillshare\agents\` | `%AppData%\skillshare\plugins\` | `%AppData%\skillshare\hooks\` | `%AppData%\skillshare\extras\` | NTFS Junctions (no admin required) |

### Native integrations

Plugins and hooks are separate from skills, agents, and extras:

- **Plugins** are native Claude/Codex plugin bundles that render into target-specific marketplace roots and can be enabled during sync.
- **Hooks** are standalone Claude/Codex hook bundles that render scripts into managed hook roots, then merge references back into each tool's config.
- **Scope today** — plugin and hook management currently targets **Claude** and **Codex** only.
- **Web UI** — the current web UI does not yet have dedicated plugin/hook screens; use the CLI or server API surfaces for these resources.

See the full docs: [Plugins](https://skillshare.runkids.cc/docs/reference/commands/plugins), [Hooks](https://skillshare.runkids.cc/docs/reference/commands/hooks), and [Source & Targets](https://skillshare.runkids.cc/docs/understand/source-and-targets).

| | Imperative (install-per-command) | Declarative (skillshare) |
|---|---|---|
Expand Down Expand Up @@ -203,6 +216,16 @@ skillshare extras collect rules # collect local files back to source
skillshare completion bash --install # also: zsh, fish, powershell, nushell
```

**Native plugins and hooks** —manage Claude/Codex native integrations from source

```bash
skillshare plugins list
skillshare plugins import demo --from claude
skillshare plugins sync --target all
skillshare hooks import --from codex --all
skillshare hooks sync --target claude
```

**Web dashboard** —visual control panel

```bash
Expand All @@ -211,6 +234,9 @@ skillshare ui

[All commands & guides →](https://skillshare.runkids.cc/docs/reference/commands)

> [!NOTE]
> Validation for plugin/hook docs and runtime flows is safest in an isolated Docker sandbox: mount the repo read-only, copy your `~/.config/skillshare`, `~/.claude`, `~/.codex`, and `~/.agents` state into a disposable sandbox HOME, and run verification there so host config is never mutated.

## Contributing

Contributions welcome! Open an issue first, then submit a draft PR with tests.
Expand Down
24 changes: 16 additions & 8 deletions cmd/skillshare/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ type diffRenderOpts struct {

// diffJSONOutput is the JSON representation for diff --json output.
type diffJSONOutput struct {
Targets []diffJSONTarget `json:"targets"`
Duration string `json:"duration"`
Targets []diffJSONTarget `json:"targets"`
Duration string `json:"duration"`
Plugins []pluginDiffJSONEntry `json:"plugins,omitempty"`
Hooks []hookDiffJSONEntry `json:"hooks,omitempty"`
}

type diffJSONTarget struct {
Expand Down Expand Up @@ -483,7 +485,7 @@ func cmdDiffGlobal(targetName string, kind resourceKindFilter, opts diffRenderOp
results = mergeAgentDiffsGlobal(cfg, results, targetName)

if opts.jsonOutput {
return diffOutputJSONWithExtras(results, extrasResults, start)
return diffOutputJSONWithExtras(results, extrasResults, collectPluginDiff(cfg.EffectivePluginsSource(), ""), collectHookDiff(cfg.EffectiveHooksSource(), ""), start)
}
if shouldLaunchTUI(opts.noTUI, cfg) && len(results) > 0 {
return runDiffTUI(results, extrasResults)
Expand All @@ -509,9 +511,11 @@ func diffItemToJSON(item copyDiffEntry) diffJSONItem {
}
}

func diffOutputJSON(results []targetDiffResult, start time.Time) error {
func diffOutputJSON(results []targetDiffResult, pluginResults []pluginDiffJSONEntry, hookResults []hookDiffJSONEntry, start time.Time) error {
output := diffJSONOutput{
Duration: formatDuration(start),
Plugins: pluginResults,
Hooks: hookResults,
}
for _, r := range results {
jt := diffJSONTarget{
Expand All @@ -530,15 +534,19 @@ func diffOutputJSON(results []targetDiffResult, start time.Time) error {
return writeJSON(&output)
}

func diffOutputJSONWithExtras(results []targetDiffResult, extrasResults []extraDiffResult, start time.Time) error {
func diffOutputJSONWithExtras(results []targetDiffResult, extrasResults []extraDiffResult, pluginResults []pluginDiffJSONEntry, hookResults []hookDiffJSONEntry, start time.Time) error {
type outputWithExtras struct {
Targets []diffJSONTarget `json:"targets"`
Extras []extraDiffJSONEntry `json:"extras,omitempty"`
Duration string `json:"duration"`
Targets []diffJSONTarget `json:"targets"`
Extras []extraDiffJSONEntry `json:"extras,omitempty"`
Plugins []pluginDiffJSONEntry `json:"plugins,omitempty"`
Hooks []hookDiffJSONEntry `json:"hooks,omitempty"`
Duration string `json:"duration"`
}
o := outputWithExtras{
Duration: formatDuration(start),
Extras: extrasDiffToJSON(extrasResults),
Plugins: pluginResults,
Hooks: hookResults,
}
for _, r := range results {
jt := diffJSONTarget{
Expand Down
4 changes: 2 additions & 2 deletions cmd/skillshare/diff_agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func diffProjectAgents(root, targetName string, opts diffRenderOpts, start time.
}

if opts.jsonOutput {
return diffOutputJSON(results, start)
return diffOutputJSON(results, nil, nil, start)
}

if len(results) == 0 {
Expand Down Expand Up @@ -79,7 +79,7 @@ func diffGlobalAgents(cfg *config.Config, targetName string, opts diffRenderOpts
}

if opts.jsonOutput {
return diffOutputJSON(results, start)
return diffOutputJSON(results, nil, nil, start)
}

if len(results) == 0 {
Expand Down
64 changes: 64 additions & 0 deletions cmd/skillshare/diff_plugin_hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"os"
hookpkg "skillshare/internal/hooks"
pluginpkg "skillshare/internal/plugins"
)

type pluginDiffJSONEntry struct {
Name string `json:"name"`
Target string `json:"target"`
Synced bool `json:"synced"`
Items []string `json:"items,omitempty"`
}

type hookDiffJSONEntry struct {
Name string `json:"name"`
Target string `json:"target"`
Synced bool `json:"synced"`
Items []string `json:"items,omitempty"`
}

func collectPluginDiff(sourceRoot, projectRoot string) []pluginDiffJSONEntry {
bundles, _ := pluginpkg.Discover(sourceRoot)
var out []pluginDiffJSONEntry
for _, bundle := range bundles {
for _, target := range pluginpkg.SupportedTargets(bundle) {
rendered := pluginpkg.RenderRoot(projectRoot, bundle.Name, target)
_, err := os.Stat(rendered)
out = append(out, pluginDiffJSONEntry{
Name: bundle.Name,
Target: target,
Synced: err == nil,
Items: diffItemsForMissing(err, rendered),
})
}
}
return out
}

func collectHookDiff(sourceRoot, projectRoot string) []hookDiffJSONEntry {
bundles, _ := hookpkg.Discover(sourceRoot)
var out []hookDiffJSONEntry
for _, bundle := range bundles {
for _, target := range hookpkg.SupportedTargets(bundle) {
root := hookpkg.RenderRoot(projectRoot, bundle.Name, target)
_, err := os.Stat(root)
out = append(out, hookDiffJSONEntry{
Name: bundle.Name,
Target: target,
Synced: err == nil,
Items: diffItemsForMissing(err, root),
})
}
}
return out
}

func diffItemsForMissing(err error, path string) []string {
if err == nil {
return nil
}
return []string{"missing rendered state: " + path}
}
52 changes: 52 additions & 0 deletions cmd/skillshare/diff_plugin_hook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"os"
"path/filepath"
"testing"
)

func TestCollectPluginAndHookDiffOnlyIncludesSupportedTargets(t *testing.T) {
root := t.TempDir()
pluginSource := filepath.Join(root, "plugins")
hookSource := filepath.Join(root, "hooks")

if err := os.MkdirAll(filepath.Join(pluginSource, "claude-only", ".claude-plugin"), 0o755); err != nil {
t.Fatalf("mkdir plugin: %v", err)
}
if err := os.WriteFile(filepath.Join(pluginSource, "claude-only", ".claude-plugin", "plugin.json"), []byte(`{"name":"claude-only"}`), 0o644); err != nil {
t.Fatalf("write plugin manifest: %v", err)
}
if err := os.MkdirAll(filepath.Join(pluginSource, "generated"), 0o755); err != nil {
t.Fatalf("mkdir generated plugin: %v", err)
}
if err := os.WriteFile(filepath.Join(pluginSource, "generated", "skillshare.plugin.yaml"), []byte("shared:\n name: generated\n"), 0o644); err != nil {
t.Fatalf("write sidecar: %v", err)
}

if err := os.MkdirAll(filepath.Join(hookSource, "claude-only"), 0o755); err != nil {
t.Fatalf("mkdir hook: %v", err)
}
if err := os.WriteFile(filepath.Join(hookSource, "claude-only", "hook.yaml"), []byte("claude:\n events:\n SessionStart:\n - command: \"{HOOK_ROOT}/scripts/start.sh\"\n"), 0o644); err != nil {
t.Fatalf("write hook manifest: %v", err)
}

pluginDiff := collectPluginDiff(pluginSource, "")
if len(pluginDiff) != 4 {
t.Fatalf("expected 3 plugin diff entries, got %+v", pluginDiff)
}
targets := map[string]bool{}
for _, entry := range pluginDiff {
if entry.Name == "claude-only" {
targets[entry.Target] = true
}
}
if !targets["claude"] || !targets["codex"] {
t.Fatalf("expected generated plugin targets for claude-only bundle, got %+v", pluginDiff)
}

hookDiff := collectHookDiff(hookSource, "")
if len(hookDiff) != 1 || hookDiff[0].Target != "claude" {
t.Fatalf("expected only claude hook diff entry, got %+v", hookDiff)
}
}
2 changes: 1 addition & 1 deletion cmd/skillshare/diff_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func cmdDiffProject(root, targetName string, kind resourceKindFilter, opts diffR
results = mergeAgentDiffsProject(root, results, targetName)

if opts.jsonOutput {
return diffOutputJSONWithExtras(results, extrasResults, start)
return diffOutputJSONWithExtras(results, extrasResults, collectPluginDiff(config.PluginsSourceDirProject(root), root), collectHookDiff(config.HooksSourceDirProject(root), root), start)
}
if shouldLaunchTUI(opts.noTUI, nil) && len(results) > 0 {
return runDiffTUI(results, extrasResults)
Expand Down
Loading