1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
#!/bin/bash
# Agent skills, into every installed agent. Dry run by default; pass `yes` to
# apply.
set -euo pipefail
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib.sh
source "$(dirname "$0")/lib.sh" "$@"
# Agent skills (portable SKILL.md format), linked per skill into every installed
# agent's skills dir, so skills from plugins or other tooling are left alone.
# Two sources: this repo's skills/, and ~/.config/agent-skills.d/, where another
# layer links skills of its own.
# Agents that copy skills rather than follow links are not covered.
#
# Each entry is "<agent dir>:<skills dir>"; only where the agent dir already
# exists, so machines without a given agent stay clean.
#
# Guards, because link() starts with `rm -rf`:
# - a real file or directory in an agent's skills dir (Claude Code keeps its
# own `synced/` there) is never replaced; the skill is skipped, loudly.
# - a skill name defined in both sources is linked from neither, and said so
# once, rather than one silently winning.
# Pruned: dangling links in the hook dir, and dangling links in an agent's
# skills dir that point into either source -- a skill removed from a layer goes.
# Nothing else is touched.
hook_dir="$xdg_config_dir/agent-skills.d"
own_dir="$dotfiles_dir/skills"
# Remove $1 if it is a symlink whose target is gone and whose recorded target
# starts with one of the remaining arguments.
prune_dangling() {
local entry="$1" prefix target
shift
[[ -L "$entry" && ! -e "$entry" ]] || return 0
target="$(readlink "$entry")"
for prefix in "$@"
do
if [[ "$target" == "$prefix"/* ]]
then
if [[ -n "${is_dry_run}" ]]
then
yellow "$entry dangles and will be removed"
else
rm "$entry"
fi
return 0
fi
done
}
if [[ -d "$hook_dir" ]]
then
for entry in "$hook_dir"/*
do
[[ -L "$entry" && ! -e "$entry" ]] || continue
if [[ -n "${is_dry_run}" ]]
then
yellow "$entry dangles and will be removed"
else
rm "$entry"
fi
done
fi
# Names defined in both sources, reported once and skipped for every agent.
duplicates=" "
if [[ -d "$hook_dir" && -d "$own_dir" ]]
then
for skill in "$hook_dir"/*
do
[[ -e "$skill" ]] || continue
name="$(basename "$skill")"
if [[ -e "$own_dir/$name" ]]
then
red "skill $name is defined in both $own_dir and $hook_dir; linking neither"
duplicates="$duplicates$name "
fi
done
fi
kimi_home="${KIMI_CODE_HOME:-$HOME/.local/share/kimi}"
[[ -d "$kimi_home" ]] || kimi_home="$HOME/.kimi-code"
for entry in \
"$HOME/.claude:$HOME/.claude/skills" \
"$HOME/.pi/agent:$HOME/.pi/agent/skills" \
"$HOME/.codex:$HOME/.codex/skills" \
"$HOME/.config/opencode:$HOME/.config/opencode/skills" \
"$HOME/.cursor:$HOME/.cursor/skills" \
"$HOME/.gemini:$HOME/.gemini/skills" \
"$HOME/.copilot:$HOME/.copilot/skills" \
"$HOME/.grok:$HOME/.grok/skills" \
"$kimi_home:$kimi_home/skills"
do
agent_dir="${entry%%:*}"
skills_dir="${entry#*:}"
[[ -d "$agent_dir" ]] || continue
if [[ -d "$skills_dir" ]]
then
for existing in "$skills_dir"/*
do
prune_dangling "$existing" "$own_dir" "$hook_dir"
done
fi
for source_dir in "$own_dir" "$hook_dir"
do
[[ -d "$source_dir" ]] || continue
for skill in "$source_dir"/*
do
[[ -e "$skill" ]] || continue
name="$(basename "$skill")"
if [[ "$duplicates" == *" $name "* ]]
then
continue
fi
dest="$skills_dir/$name"
if [[ -e "$dest" && ! -L "$dest" ]]
then
red "$dest is not a link; leaving it, skill $name not linked"
continue
fi
link "$skill" "$dest"
done
done
done
|