t3-provision-users: install_skills heals stale symlinks + owns ~/.agents
All checks were successful
ci/woodpecker/push/default Pipeline was successful

Follow-up to the vendored-skills change, from verifying the emo rollout:

- The if-absent guard treated ANY pre-existing ~/.claude/skills/<name> entry
  as "installed", so a manual cross-user symlink emo already had (grill-me ->
  /home/wizard/.claude/skills/grill-me) was skipped — leaving the requested
  skill depending on the admin's home instead of emo's own copy. The guard now
  keys on the user's OWN copy (a real dir under ~/.agents/skills) and (re)points
  the ~/.claude/skills symlink at it, healing a stale/cross-user link while
  still never clobbering a real dir.
- install -d left the intermediate ~/.agents owned by root; now owned by the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-23 09:27:31 +00:00
parent 987fdd16db
commit 1c8dc6bd6c

View file

@ -438,8 +438,9 @@ install_memory() {
# `npx skills` upstream drifted off this exact set, so we reproduce it offline + deterministically. # `npx skills` upstream drifted off this exact set, so we reproduce it offline + deterministically.
# if-absent + ADDITIVE: copies a skill dir into ~/.agents/skills/<name> (owned by the user) and # if-absent + ADDITIVE: copies a skill dir into ~/.agents/skills/<name> (owned by the user) and
# symlinks ~/.claude/skills/<name> -> ../../.agents/skills/<name> (the layout `skills add -g` # symlinks ~/.claude/skills/<name> -> ../../.agents/skills/<name> (the layout `skills add -g`
# produces; Claude Code reads ~/.claude/skills/). Scoped to SKILL_USERS; never clobbers an existing # produces; Claude Code reads ~/.claude/skills/). Scoped to SKILL_USERS. if-absent keys on the
# skill. Best-effort tail: must return 0 or set -euo pipefail aborts the whole reconcile. # user's OWN copy, so it heals a stale/cross-user ~/.claude/skills symlink but never clobbers a real
# skill dir. Best-effort tail: must return 0 or set -euo pipefail aborts the whole reconcile.
install_skills() { install_skills() {
local user="$1" home local user="$1" home
home="$(getent passwd "$user" | cut -d: -f6)" home="$(getent passwd "$user" | cut -d: -f6)"
@ -456,24 +457,32 @@ install_skills() {
fi fi
local agents_dir="$home/.agents/skills" claude_dir="$home/.claude/skills" local agents_dir="$home/.agents/skills" claude_dir="$home/.claude/skills"
install -d -o "$user" -g "$user" -m 0755 "$agents_dir" "$claude_dir" # own the parent ~/.agents too (install -d leaves created intermediates root-owned)
install -d -o "$user" -g "$user" -m 0755 "$home/.agents" "$agents_dir" "$claude_dir"
chown "$user:$user" "$home/.agents" || true
local skill name dst n=0 local skill name dst link n=0
for skill in "$src_root"/*/; do for skill in "$src_root"/*/; do
[[ -d "$skill" ]] || continue [[ -d "$skill" ]] || continue
name="$(basename "$skill")" name="$(basename "$skill")"
dst="$agents_dir/$name" dst="$agents_dir/$name"
[[ -e "$dst" || -L "$claude_dir/$name" ]] && continue # if-absent: already installed link="$claude_dir/$name"
if cp -a "$src_root/$name" "$dst"; then # if-absent keys on the user's OWN copy (a real dir under ~/.agents/skills), NOT on any
chown -R "$user:$user" "$dst" # pre-existing ~/.claude/skills entry — so a stale or cross-user symlink gets healed.
ln -sfn "../../.agents/skills/$name" "$claude_dir/$name" if [[ ! -d "$dst" ]]; then
chown -h "$user:$user" "$claude_dir/$name" cp -a "$src_root/$name" "$dst" || { log "WARN: copy skill $name -> $user failed"; continue; }
chown -R "$user:$user" "$dst" || true
n=$((n+1)) n=$((n+1))
else fi
log "WARN: copy skill $name -> $user failed" # point ~/.claude/skills/<name> at the user's own copy (replacing a stale/cross-user symlink);
# never clobber a real dir/file squatting that name.
if [[ -d "$link" && ! -L "$link" ]]; then
log "WARN: $claude_dir/$name is a real dir (left as-is) for $user"
elif [[ "$(readlink "$link" 2>/dev/null)" != "../../.agents/skills/$name" ]]; then
ln -sfn "../../.agents/skills/$name" "$link" && chown -h "$user:$user" "$link" || log "WARN: link skill $name -> $user failed"
fi fi
done done
if [[ "$n" -gt 0 ]]; then log "vendored $n skill(s) -> $user"; fi if [[ "$n" -gt 0 ]]; then log "vendored/healed $n skill(s) -> $user"; fi
return 0 # best-effort tail must never return non-zero, else set -euo pipefail aborts the reconcile return 0 # best-effort tail must never return non-zero, else set -euo pipefail aborts the reconcile
} }