Skip to content

Commit 19fa98e

Browse files
authored
fix: agent skills discovery and syncing (#722)
* fix: agent skills discovery and syncing * fix: claude plgin skills * feat: codex hidden (general) case handled, + simlinks * feat: enrich agent skill with description and display name metadata * fix: linter * refactor: remove unused truncateSourceID
1 parent 1da9e89 commit 19fa98e

9 files changed

Lines changed: 956 additions & 2 deletions

File tree

cmd/endpoint/scan.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ type Options struct {
6363
// It is the integration point for cmd/ai/discover; opts.Kinds is
6464
// overwritten.
6565
func RunAITool(ctx context.Context, opts Options) error {
66-
opts.Kinds = []string{scanners.KindAITool}
66+
opts.Kinds = []string{
67+
scanners.KindAITool,
68+
scanners.KindAgentSkill,
69+
}
6770
return runScan(ctx, opts)
6871
}
6972

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Design: Agent Skills Scanner
2+
3+
**Date:** 2026-05-08
4+
**Status:** Approved
5+
6+
## Problem
7+
8+
`vet endpoint scan` (and its alias `vet ai discover`) discovers installed AI tools, MCP servers, coding agents, and extensions — but not **skills** (agent skill directories). Skills are a first-class artifact of AI coding agents (Claude Code, Cursor, Windsurf, etc.) and should be part of the endpoint inventory synced to SafeDep Cloud.
9+
10+
## Goal
11+
12+
Extend `vet endpoint scan` and `vet ai discover` to also discover and sync agent skills installed on the endpoint. No new commands, no new sinks, no new flags — pure extension of the existing inventory pipeline.
13+
14+
## Scope
15+
16+
- Scan project-local skill directories (e.g., `.claude/skills/`) and global skill directories (e.g., `~/.claude/skills/`)
17+
- Each subdirectory = one skill; directory name = skill name
18+
- Emit one `inventory.Item` per discovered skill directory
19+
- Register as a new scanner kind `"agent-skill"` in the existing registry
20+
- `vet ai discover` pins to both `ai-tool` and `agent-skill` kinds
21+
- Cloud sync, local table, JSON report all work automatically through existing sinks
22+
23+
## Out of Scope
24+
25+
- Parsing skill file contents or frontmatter
26+
- Detecting enabled/disabled state
27+
- Any new CLI commands or flags
28+
29+
## Architecture
30+
31+
Pure extension following the `aitool` scanner pattern:
32+
33+
```
34+
pkg/inventory/scanners/
35+
├── scanners.go ← add KindAgentSkill constant + descriptor
36+
└── skills/
37+
├── adapter.go ← implements inventory.Scanner
38+
└── translate.go ← skill discovery result → *inventory.Item
39+
```
40+
41+
`cmd/endpoint/scan.go`: update `RunAITool` to pin `[]string{KindAITool, KindAgentSkill}`.
42+
43+
No changes to orchestrator, sinks, or any other command.
44+
45+
## Agent Registry
46+
47+
Each agent entry encodes the project-local path (relative to `cfg.ProjectDir`) and the global path (relative to `cfg.HomeDir`). Both paths are checked when the corresponding scope is enabled.
48+
49+
Supported agents (from the canonical table):
50+
51+
| Agent | Kind Flag | Project Path | Global Path |
52+
|---|---|---|---|
53+
| Amp, Kimi CLI, Replit, Universal | `amp`, `kimi-cli`, `replit`, `universal` | `.agents/skills/` | `~/.config/agents/skills/` |
54+
| Antigravity | `antigravity` | `.agents/skills/` | `~/.gemini/antigravity/skills/` |
55+
| Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
56+
| IBM Bob | `bob` | `.bob/skills/` | `~/.bob/skills/` |
57+
| Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
58+
| OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
59+
| Cline, Warp | `cline`, `warp` | `.agents/skills/` | `~/.agents/skills/` |
60+
| CodeBuddy | `codebuddy` | `.codebuddy/skills/` | `~/.codebuddy/skills/` |
61+
| Codex | `codex` | `.agents/skills/` | `~/.codex/skills/` |
62+
| Command Code | `command-code` | `.commandcode/skills/` | `~/.commandcode/skills/` |
63+
| Continue | `continue` | `.continue/skills/` | `~/.continue/skills/` |
64+
| Cortex Code | `cortex` | `.cortex/skills/` | `~/.snowflake/cortex/skills/` |
65+
| Crush | `crush` | `.crush/skills/` | `~/.config/crush/skills/` |
66+
| Cursor | `cursor` | `.agents/skills/` | `~/.cursor/skills/` |
67+
| Deep Agents | `deepagents` | `.agents/skills/` | `~/.deepagents/agent/skills/` |
68+
| Droid | `droid` | `.factory/skills/` | `~/.factory/skills/` |
69+
| Firebender | `firebender` | `.agents/skills/` | `~/.firebender/skills/` |
70+
| Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
71+
| GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
72+
| Goose | `goose` | `.goose/skills/` | `~/.config/goose/skills/` |
73+
| Junie | `junie` | `.junie/skills/` | `~/.junie/skills/` |
74+
| iFlow CLI | `iflow-cli` | `.iflow/skills/` | `~/.iflow/skills/` |
75+
| Kilo Code | `kilo` | `.kilocode/skills/` | `~/.kilocode/skills/` |
76+
| Kiro CLI | `kiro-cli` | `.kiro/skills/` | `~/.kiro/skills/` |
77+
| Kode | `kode` | `.kode/skills/` | `~/.kode/skills/` |
78+
| MCPJam | `mcpjam` | `.mcpjam/skills/` | `~/.mcpjam/skills/` |
79+
| Mistral Vibe | `mistral-vibe` | `.vibe/skills/` | `~/.vibe/skills/` |
80+
| Mux | `mux` | `.mux/skills/` | `~/.mux/skills/` |
81+
| OpenCode | `opencode` | `.agents/skills/` | `~/.config/opencode/skills/` |
82+
| OpenHands | `openhands` | `.openhands/skills/` | `~/.openhands/skills/` |
83+
| Pi | `pi` | `.pi/skills/` | `~/.pi/agent/skills/` |
84+
| Qoder | `qoder` | `.qoder/skills/` | `~/.qoder/skills/` |
85+
| Qwen Code | `qwen-code` | `.qwen/skills/` | `~/.qwen/skills/` |
86+
| Roo Code | `roo` | `.roo/skills/` | `~/.roo/skills/` |
87+
| Trae | `trae` | `.trae/skills/` | `~/.trae/skills/` |
88+
| Trae CN | `trae-cn` | `.trae/skills/` | `~/.trae-cn/skills/` |
89+
| Windsurf | `windsurf` | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
90+
| Zencoder | `zencoder` | `.zencoder/skills/` | `~/.zencoder/skills/` |
91+
| Neovate | `neovate` | `.neovate/skills/` | `~/.neovate/skills/` |
92+
| Pochi | `pochi` | `.pochi/skills/` | `~/.pochi/skills/` |
93+
| AdaL | `adal` | `.adal/skills/` | `~/.adal/skills/` |
94+
95+
Note: agents sharing the same project path (e.g., `amp`, `kimi-cli`, `replit`, `universal` all use `.agents/skills/`) are treated as separate entries — the `App` field on the emitted `Item` records which agent name the skill belongs to. Skills found in a shared path are emitted once per matching agent.
96+
97+
## Data Model
98+
99+
Each discovered skill directory produces one `*inventory.Item`:
100+
101+
```
102+
Kind = KindAgentSkill (9)
103+
Name = directory name (e.g. "yaad", "stop-slop")
104+
App = agent identifier (e.g. "claude-code")
105+
Scope = ScopeProject | ScopeSystem
106+
ConfigPath = absolute path to skill directory
107+
ItemIdentity = FNV-64a( app + "/" + kind + "/" + scope + "/" + name + "/" + configPath )
108+
Enabled = nil
109+
Metadata = nil
110+
```
111+
112+
## Scanner Logic
113+
114+
```
115+
for each agentEntry in registry:
116+
for each (scope, dir) in [(ScopeProject, projectDir/agentEntry.ProjectPath),
117+
(ScopeSystem, homeDir/agentEntry.GlobalPath)]:
118+
if !cfg.ScopeEnabled(scope): continue
119+
if dir does not exist: continue
120+
for each entry in ReadDir(dir):
121+
if !entry.IsDir(): continue
122+
emit translate(agentEntry.App, scope, entry.Name(), dir/entry.Name())
123+
```
124+
125+
## Item Identity
126+
127+
Follows the same FNV-64a scheme used by the aitool scanner:
128+
129+
```go
130+
ItemIdentity = fmt.Sprintf("%x", fnv64a(app + "/" + kindStr + "/" + scopeStr + "/" + name + "/" + configPath))
131+
```
132+
133+
## Changes Summary
134+
135+
| File | Change |
136+
|---|---|
137+
| `pkg/inventory/scanners/scanners.go` | Add `KindAgentSkill = "agent-skill"` constant; add descriptor to registry |
138+
| `pkg/inventory/scanners/skills/adapter.go` | New — Scanner implementation |
139+
| `pkg/inventory/scanners/skills/translate.go` | New — skill → Item translation |
140+
| `cmd/endpoint/scan.go` | `RunAITool` pins `[]string{KindAITool, KindAgentSkill}` |
141+
142+
## Testing
143+
144+
- Unit tests in `pkg/inventory/scanners/skills/adapter_test.go` using a temp directory tree
145+
- Unit tests in `pkg/inventory/scanners/skills/translate_test.go` for Item field correctness
146+
- Existing scenario tests and orchestrator tests are unaffected

pkg/inventory/scanners/scanners.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/safedep/vet/pkg/aitool"
1818
"github.com/safedep/vet/pkg/inventory"
1919
aitoolscanner "github.com/safedep/vet/pkg/inventory/scanners/aitool"
20+
skillsscanner "github.com/safedep/vet/pkg/inventory/scanners/skills"
2021
)
2122

2223
// Descriptor declares one scanner: the Kind token accepted on the
@@ -31,7 +32,8 @@ type Descriptor struct {
3132
// Kind values accepted on the --kind flag and consumed by callers that
3233
// pin a specific scanner (e.g. cmd/ai/discover).
3334
const (
34-
KindAITool = "ai-tool"
35+
KindAITool = "ai-tool"
36+
KindAgentSkill = "agent-skill"
3537
)
3638

3739
// registry is the shipped set of scanner declarations. Adding a scanner
@@ -43,6 +45,12 @@ var registry = []Descriptor{
4345
return aitoolscanner.New(aitool.DefaultRegistry())
4446
},
4547
},
48+
{
49+
Kind: KindAgentSkill,
50+
New: func() inventory.Scanner {
51+
return skillsscanner.New()
52+
},
53+
},
4654
}
4755

4856
// Build returns the scanners matching the requested kind allowlist. An

0 commit comments

Comments
 (0)