From 1c8dc6bd6cac85fcf4e7906da3af3632d9d4080c Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Tue, 23 Jun 2026 09:27:31 +0000 Subject: [PATCH] t3-provision-users: install_skills heals stale symlinks + owns ~/.agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the vendored-skills change, from verifying the emo rollout: - The if-absent guard treated ANY pre-existing ~/.claude/skills/ 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 --- scripts/t3-provision-users.sh | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/scripts/t3-provision-users.sh b/scripts/t3-provision-users.sh index d4e997d1..9cbc6c1e 100644 --- a/scripts/t3-provision-users.sh +++ b/scripts/t3-provision-users.sh @@ -438,8 +438,9 @@ install_memory() { # `npx skills` upstream drifted off this exact set, so we reproduce it offline + deterministically. # if-absent + ADDITIVE: copies a skill dir into ~/.agents/skills/ (owned by the user) and # symlinks ~/.claude/skills/ -> ../../.agents/skills/ (the layout `skills add -g` -# produces; Claude Code reads ~/.claude/skills/). Scoped to SKILL_USERS; never clobbers an existing -# skill. Best-effort tail: must return 0 or set -euo pipefail aborts the whole reconcile. +# produces; Claude Code reads ~/.claude/skills/). Scoped to SKILL_USERS. if-absent keys on the +# 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() { local user="$1" home home="$(getent passwd "$user" | cut -d: -f6)" @@ -456,24 +457,32 @@ install_skills() { fi 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 [[ -d "$skill" ]] || continue name="$(basename "$skill")" dst="$agents_dir/$name" - [[ -e "$dst" || -L "$claude_dir/$name" ]] && continue # if-absent: already installed - if cp -a "$src_root/$name" "$dst"; then - chown -R "$user:$user" "$dst" - ln -sfn "../../.agents/skills/$name" "$claude_dir/$name" - chown -h "$user:$user" "$claude_dir/$name" + link="$claude_dir/$name" + # if-absent keys on the user's OWN copy (a real dir under ~/.agents/skills), NOT on any + # pre-existing ~/.claude/skills entry — so a stale or cross-user symlink gets healed. + if [[ ! -d "$dst" ]]; then + cp -a "$src_root/$name" "$dst" || { log "WARN: copy skill $name -> $user failed"; continue; } + chown -R "$user:$user" "$dst" || true n=$((n+1)) - else - log "WARN: copy skill $name -> $user failed" + fi + # point ~/.claude/skills/ 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 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 }