| name | contribute-skill |
| description | ローカルで改修した `skills/<skill-name>/` を upstream リポジトリ (Fandhe-AI/agent-cli-skills 等) へ PR として投稿する。`skills-lock.json` の `source` を読み、`Fandhe-AI/` 以外への push は安全弁で中止。clone → 反映 → セキュリティチェック → ブランチ作成 → push → `gh pr create` を実行。マージ後は sync-skills-lock で hash 更新。「スキルを upstream に貢献」「外部リポジトリに PR」などで使用。 |
| argument-hint | <skill-name> (例: contribute-skill create-pr) |
| user-invocable | true |
| model | sonnet |
contribute-skill
ローカルで改修した skills/<skill-name>/ を、skills-lock.json に記録された upstream リポジトリへ PR として投稿します。
前提条件
gh CLI がインストールされ認証済みであること(対象 org への push / PR 権限が必要)
- 対象スキルが
skills-lock.json に登録されていること
- 対象スキルのローカル改修が最新のコミットに含まれ、作業ツリーが clean であること
責務の分離
- create-pr: 現在のリポジトリ内でカレントブランチから base へ PR を作成する
- contribute-skill: 別リポジトリ(upstream)へ clone → 変更反映 → push → PR 作成を行う
外部リポジトリ貢献は clone / path 変換 / 異なる認証境界が関わるため、別スキルとして分離しています。
フロー
Step 1: 引数を検証する
SKILL_NAME="$ARGUMENTS"
if [[ -z "${SKILL_NAME}" ]]; then
echo "対象スキルを指定してください。候補:"
ls -1 skills/ 2>/dev/null
ls -1 .agents/skills/ 2>/dev/null
[[ ! -L .claude && ! -L .claude/skills ]] && find .claude/skills -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sed 's|.*/||'
echo "(lockfile 由来のスキルは .agents/skills/ のみ、リポジトリ管理スキルは .claude/skills/ のみに存在する場合がある)"
exit 1
fi
if [[ ! "${SKILL_NAME}" =~ ^[a-z][a-z0-9-]+$ ]]; then
echo "エラー: SKILL_NAME は小文字 kebab-case のみ許可されています: ${SKILL_NAME}"
exit 1
fi
assert_no_symlink_components() {
local rel="$1" prefix="${2:+${2}/}" acc="" part
local -a parts
SYMLINK_COMPONENT=""
IFS='/' read -r -a parts <<< "${rel}"
for part in "${parts[@]}"; do
acc="${acc:+${acc}/}${part}"
if [[ -L "${prefix}${acc}" ]]; then
SYMLINK_COMPONENT="${prefix}${acc}"
return 1
fi
done
return 0
}
if [[ -n "${LOCAL_SKILL_DIR:-}" ]]; then
case "${LOCAL_SKILL_DIR}" in
"skills/${SKILL_NAME}"|".agents/skills/${SKILL_NAME}"|".claude/skills/${SKILL_NAME}") ;;
*)
echo "エラー: LOCAL_SKILL_DIR は skills/${SKILL_NAME} / .agents/skills/${SKILL_NAME} / .claude/skills/${SKILL_NAME} のいずれかを指定してください: ${LOCAL_SKILL_DIR}"
exit 1
;;
esac
if ! assert_no_symlink_components "${LOCAL_SKILL_DIR}"; then
echo "エラー: LOCAL_SKILL_DIR の経路に symlink が含まれています。実体側のパスを指定してください: ${SYMLINK_COMPONENT} -> $(readlink "${SYMLINK_COMPONENT}")"
exit 1
fi
if [[ ! -d "${LOCAL_SKILL_DIR}" ]]; then
echo "エラー: 指定された LOCAL_SKILL_DIR が存在しません: ${LOCAL_SKILL_DIR}"
exit 1
fi
else
have_skills=0; have_agents=0; have_claude=0
[[ -d "skills/${SKILL_NAME}" ]] && assert_no_symlink_components "skills/${SKILL_NAME}" && have_skills=1
[[ -d ".agents/skills/${SKILL_NAME}" ]] && assert_no_symlink_components ".agents/skills/${SKILL_NAME}" && have_agents=1
[[ -d ".claude/skills/${SKILL_NAME}" ]] && assert_no_symlink_components ".claude/skills/${SKILL_NAME}" && have_claude=1
if (( have_skills + have_agents + have_claude > 1 )); then
echo "エラー: ${SKILL_NAME} の実体が skills/ / .agents/skills/ / .claude/skills/ の複数に存在します。"
echo "環境変数 LOCAL_SKILL_DIR にどれかを指定して再実行してください(例: LOCAL_SKILL_DIR=.agents/skills/${SKILL_NAME})。"
exit 1
elif [[ "${have_skills}" -eq 1 ]]; then
LOCAL_SKILL_DIR="skills/${SKILL_NAME}"
elif [[ "${have_agents}" -eq 1 ]]; then
LOCAL_SKILL_DIR=".agents/skills/${SKILL_NAME}"
elif [[ "${have_claude}" -eq 1 ]]; then
LOCAL_SKILL_DIR=".claude/skills/${SKILL_NAME}"
else
echo "エラー: ローカルスキルが見つかりません: skills/${SKILL_NAME} / .agents/skills/${SKILL_NAME} / .claude/skills/${SKILL_NAME}"
exit 1
fi
fi
引数が空の場合はパス解決に進まず、skills/・.agents/skills/・.claude/skills/(実ディレクトリのみ。symlink は実体側の候補に現れるため除外)の候補一覧を表示して終了します。Claude はその一覧をユーザーに提示し、スキル名を選んでもらってから再実行を促してください。後続の Step では ${LOCAL_SKILL_DIR}/ を使ってローカルパスを参照します。
skills/・.agents/skills/・.claude/skills/(実ディレクトリのみ。symlink は実体側でカウント)の複数にディレクトリが存在する場合は中止し、環境変数 LOCAL_SKILL_DIR に改修対象のパス(skills/<name>・.agents/skills/<name>・.claude/skills/<name> のいずれか)を指定して再実行するよう案内します(silently に skills/ を優先しません)。環境変数 LOCAL_SKILL_DIR が設定済みの場合は、許可された3パスのいずれかであること・経路の全要素(中間の親ディレクトリ含む)が symlink でないこと・実在することを検証してから採用し、自動解決をスキップします。いずれにも存在しなければエラーで中止します。
Step 2: upstream を特定する
ルートの skills-lock.json を読み、skills.<SKILL_NAME>.source を取り出します。
SOURCE=$(jq -r ".skills[\"${SKILL_NAME}\"].source" skills-lock.json)
SOURCE_TYPE=$(jq -r ".skills[\"${SKILL_NAME}\"].sourceType" skills-lock.json)
case "${SOURCE}" in
https://github.com/*)
REPO_SLUG="${SOURCE#https://github.com/}"
;;
*)
REPO_SLUG="${SOURCE}"
;;
esac
REPO_SLUG="${REPO_SLUG%.git}"
if [[ ! "${REPO_SLUG}" =~ ^Fandhe-AI/[A-Za-z0-9._-]+$ ]]; then
echo "エラー: source '${SOURCE}' は Fandhe-AI/<repo> 形式ではありません。中止します。"
exit 1
fi
case "${REPO_SLUG#Fandhe-AI/}" in
.|..)
echo "エラー: source '${SOURCE}' の repository 名が不正です。中止します。"
1
;;
[[ != ]];
1
source は正規化後の OWNER/REPO が ^Fandhe-AI/[A-Za-z0-9._-]+$ に完全一致する場合のみ許可します(安全弁:見知らぬリポジトリへ意図せず push しないため)。../ を含むパストラバーサル・クエリ(?x=1)・フラグメント(#frag)・余剰パスセグメント(/extra)を含む値は正規表現に一致せずエラーで中止します。検証は必ず .git 除去などの正規化の後に行います(正規化前に検証すると Fandhe-AI/..git のような値が正規化後に別の値へ化けてすり抜けるため)。
sourceType が github 以外の場合も エラーで中止 します(GitHub 以外の source は本スキルの想定外であり、gh repo clone / gh pr create が正常動作しないため)。
- 正規化後の
REPO_SLUG は以降の Step で gh repo clone・gh pr create --repo に利用します。
Step 3: 変更内容を確認する
git log --oneline -- "${LOCAL_SKILL_DIR}/"
if git rev-parse --verify -q HEAD~1 >/dev/null; then
git diff HEAD~1 HEAD -- "${LOCAL_SKILL_DIR}/"
else
git diff "$(git hash-object -t tree /dev/null)" HEAD -- "${LOCAL_SKILL_DIR}/"
fi
ユーザーに「この改修内容で upstream に PR を作ってよいか」を確認します。
Step 4: セキュリティチェック(必須)
create-pr と同様に以下をレビューします。
- 認証・認可の実装漏れ
- API キー・シークレットのハードコーディング
- XSS の可能性(ドキュメントでも外部埋め込みが含まれる場合)
- 入力バリデーションの欠如
- OWASP Top 10
問題があれば upstream 貢献を中止し、ユーザーに警告します。
Step 5: 変更を反映する
skills-contribute.sh は upstream の gh repo clone から作業ディレクトリ(WORKDIR)の作成・反映までを自己完結で行います。手動での事前 clone は不要です(機械可読な CONTRIBUTE_SKILL_WORKDIR= 等の出力を Step 6 以降で唯一の正として使う契約に一本化しています)。
このステップは手順を個別に打鍵せず、必ず本スキル自身のスクリプト(skills-contribute.sh)を実行してください。 同スクリプトには rm -rf 前の symlink 境界検証(TOCTOU 対策込み)が実装されており、以下の断片だけを個別に実行すると検証が欠落します。
skills-contribute.sh は upstream 側でスキルがどのパス構造に置かれているか(UPSTREAM_SKILL_PATH。skills-lock.json の skillPath はローカル install パスであり upstream 内の配置ではないため使用しません)の判定と、cp -R の delete-then-copy 反映を内部で行う自己完結型スクリプトです。判定・反映のロジックは後述の参考コードのとおりです。
LOCAL_SKILL_DIR は Step 1 で解決した貢献対象スキル($ARGUMENTS)のパスであり、本スキル(contribute-skill)自身の配置とは無関係です。スクリプトの実行パスに LOCAL_SKILL_DIR を流用すると、貢献対象が contribute-skill 以外の場合に存在しないパスを参照してしまいます。実行するスクリプト自身の配置は別変数 CONTRIBUTE_SKILL_DIR として、本スキル(contribute-skill)自身のインストール場所から解決してください。
skills-contribute.sh は呼び出し時のカレントディレクトリを貢献元リポジトリのルートとして LOCAL_SKILL_DIR・skills-lock.json を探索し、内部で自分自身の gh repo clone と WORKDIR(clone 先)を新規作成します。手動での事前 clone は不要なため、実行直前にこの Step 内で ORIG_DIR(貢献元ローカルリポジトリのルート)を捕捉しておいてください。スクリプトの標準出力最終行群が返す CONTRIBUTE_SKILL_WORKDIR=<path> と CONTRIBUTE_SKILL_UPSTREAM_PATH=<path> を捕捉し、WORKDIR および(後述の参考コードで示す判定ロジックの)UPSTREAM_SKILL_PATH はこれらの値のみを唯一の正として採用します(参考コードを個別実行して得た値は使用しません)。これにより Step 6 以降が参照する ${WORKDIR}/upstream と ${UPSTREAM_SKILL_PATH} は、スクリプトが実際に使った clone・実際に反映したパスと一致します。
ORIG_DIR="$(pwd)"
if [[ -n "${CONTRIBUTE_SKILL_DIR:-}" ]]; then
if [[ "${CONTRIBUTE_SKILL_DIR}" == "${ORIG_DIR}/skills/contribute-skill" ]]; then
CONTRIBUTE_SKILL_REL="skills/contribute-skill"
elif [[ "${CONTRIBUTE_SKILL_DIR}" == "${ORIG_DIR}/.agents/skills/contribute-skill" ]]; then
CONTRIBUTE_SKILL_REL=".agents/skills/contribute-skill"
elif [[ "${CONTRIBUTE_SKILL_DIR}" == "${ORIG_DIR}/.claude/skills/contribute-skill" ]]; then
CONTRIBUTE_SKILL_REL=".claude/skills/contribute-skill"
else
echo "エラー: CONTRIBUTE_SKILL_DIR は ${ORIG_DIR}/skills/contribute-skill / ${ORIG_DIR}/.agents/skills/contribute-skill / ${ORIG_DIR}/.claude/skills/contribute-skill のいずれかを指定してください: ${CONTRIBUTE_SKILL_DIR}"
exit 1
fi
! assert_no_symlink_components ;
1
[[ ! -d ]];
1
have_contribute_skills=0; have_contribute_agents=0; have_contribute_claude=0
[[ -d ]] && assert_no_symlink_components && have_contribute_skills=1
[[ -d ]] && assert_no_symlink_components && have_contribute_agents=1
[[ -d ]] && assert_no_symlink_components && have_contribute_claude=1
(( have_contribute_skills + have_contribute_agents + have_contribute_claude > ));
1
[[ -eq 1 ]];
CONTRIBUTE_SKILL_DIR=
[[ -eq 1 ]];
CONTRIBUTE_SKILL_DIR=
[[ -eq 1 ]];
CONTRIBUTE_SKILL_DIR=
1
SCRIPT_OUTPUT=$(LOCAL_SKILL_DIR= | /dev/stderr)
SCRIPT_UPSTREAM_DIR=$( | grep | -1 | -d= -f2-)
UPSTREAM_SKILL_PATH=$( | grep | -1 | -d= -f2-)
[[ -z || ! -d ]];
1
[[ -z ]];
1
WORKDIR=
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed || )
以下は skills-contribute.sh が内部で実行する処理(UPSTREAM_SKILL_PATH の判定・delete-then-copy)の参考コードです。上記のスクリプト実行によって既に完了しているため、個別に実行する必要はありません。
if [[ -d "skills/${SKILL_NAME}" ]] && assert_no_symlink_components "skills/${SKILL_NAME}"; then
UPSTREAM_SKILL_PATH="skills/${SKILL_NAME}"
elif [[ -d ".agents/skills/${SKILL_NAME}" ]] && assert_no_symlink_components ".agents/skills/${SKILL_NAME}"; then
UPSTREAM_SKILL_PATH=".agents/skills/${SKILL_NAME}"
elif [[ -d ".claude/skills/${SKILL_NAME}" ]] && assert_no_symlink_components ".claude/skills/${SKILL_NAME}"; then
UPSTREAM_SKILL_PATH=".claude/skills/${SKILL_NAME}"
elif [[ -d "skills" ]] && assert_no_symlink_components "skills"; then
UPSTREAM_SKILL_PATH="skills/${SKILL_NAME}"
elif [[ -d ".agents/skills" ]] && assert_no_symlink_components ;
UPSTREAM_SKILL_PATH=
[[ -d ]] && assert_no_symlink_components ;
UPSTREAM_SKILL_PATH=
UPSTREAM_SKILL_PATH=
case "${UPSTREAM_SKILL_PATH}" in
"skills/${SKILL_NAME}"|".agents/skills/${SKILL_NAME}"|".claude/skills/${SKILL_NAME}") ;;
*)
echo "エラー: 想定外の UPSTREAM_SKILL_PATH です: ${UPSTREAM_SKILL_PATH}"
exit 1
;;
esac
CLONE_ROOT="${WORKDIR}/upstream"
DELETE_TARGET="${CLONE_ROOT}/${UPSTREAM_SKILL_PATH}"
CLONE_ROOT_REAL="$(cd "${CLONE_ROOT}" && pwd -P)"
if ! assert_no_symlink_components "${UPSTREAM_SKILL_PATH}" "${CLONE_ROOT}"; then
echo "エラー: 削除対象の経路に symlink が含まれています: ${SYMLINK_COMPONENT}"
exit 1
DELETE_PARENT=
DELETE_LEAF=
[[ -d ]];
(
-P -- || 1
PARENT_REAL=
*) ;;
*)
1
;;
-rf --
)
-p
-R
削除対象は必ず ${WORKDIR}/upstream/ 配下(clone 用の一時ディレクトリ)に閉じ、UPSTREAM_SKILL_PATH が skills/<name>・.agents/skills/<name>・.claude/skills/<name> の 3 形態以外なら rm -rf の前に中止します。加えて中間パスの symlink 化・TOCTOU に対する実体パス検証を rm 直前に行います。新規スキル追加(宛先未存在)の場合も削除処理は無害にスキップされ、直後の mkdir -p で作成されます。
Step 6: 差分を確認する
cd "$WORKDIR/upstream"
git status
git diff
ユーザーに差分を見せ、内容が意図通りか確認します。
Step 7: ブランチ作成・コミット
SLUG=$(date +%Y%m%d-%H%M%S)
git switch -c "contribute/${SKILL_NAME}-${SLUG}"
git add "${UPSTREAM_SKILL_PATH}/"
git commit -m "$(cat <<'EOF'
<type>(<scope>): <subject>
ローカルの skills/<SKILL_NAME>/ からの貢献。
EOF
)"
git add "${UPSTREAM_SKILL_PATH}/" はパス指定 add のため、Step 5 の delete-then-copy で消えたファイルの削除(D)も含めて stage されます
- Conventional Commits 形式
--no-verify は使用しない(pre-commit フックを通す)
- co-author は付けない(ローカル規約に合わせる)
Step 8: push と PR 作成
git push -u origin "contribute/${SKILL_NAME}-${SLUG}"
SRC_REPO=$(git -C "${ORIG_DIR}" remote get-url origin 2>/dev/null \
| sed -E 's#^(git@github\.com:|https://github\.com/)##; s#\.git$##')
[[ -n "${SRC_REPO}" ]] || SRC_REPO=$(basename "${ORIG_DIR}")
gh pr create \
--repo "${REPO_SLUG}" \
--base "${DEFAULT_BRANCH:-main}" \
--title "<type>(<scope>): <subject>" \
--body "$(cat <<'EOF'
## Summary
- <SKILL_NAME> の改修内容(箇条書き)
## Source
ローカルの <SRC_REPO> 側で改修後、`/contribute-skill <SKILL_NAME>` により投稿。
## Test plan
- [ ] SKILL.md を実際に Claude Code で実行
- [ ] Conventional Commits に沿ったメッセージ生成を確認
- [ ] エッジケース確認
EOF
)"
--repo には Step 2 で正規化した ${REPO_SLUG}(OWNER/REPO 形式)を渡します。URL 形式から OWNER/REPO への変換は Step 2 の case 文で完了しています。
body の <SRC_REPO> は上で取得した貢献元リポジトリの OWNER/REPO(origin 未設定時はディレクトリ名)に置き換えます(heredoc はクォート済みのため Claude が実値で埋める。特定リポジトリ名のハードコード禁止)。
Draft PR を作成する場合は --draft を付けます(デフォルトはユーザー確認の上で決定)。
Step 9: PR URL を返す & 後処理案内
- PR URL をユーザーに返す
- 「マージされたら
/sync-skills-lock を実行して skills-lock.json の computedHash を更新してください」と案内
- 作業用ディレクトリ
$WORKDIR は残したまま(成否が確定するまで)
注意事項
- SKILL_NAME は kebab-case のみ許可:
.. のような値によるパストラバーサルを防ぐため、空判定の直後・パス解決の前に ^[a-z][a-z0-9-]+$ で検証する(security.md A03/A01)
skills/・.agents/skills/・.claude/skills/ の複数に実体が存在する場合は中止:silently に skills/ を優先せず、環境変数 LOCAL_SKILL_DIR に改修対象パスを指定して再実行を求める。LOCAL_SKILL_DIR は skills/<name>・.agents/skills/<name>・.claude/skills/<name> の3パスのみ受理し(末尾要素・中間の親ディレクトリのいずれかが symlink なら実体側パスの指定を要求)、任意パス指定によるパストラバーサルを防ぐ。Step 5 で本スキル自身(contribute-skill)の配置を解決する CONTRIBUTE_SKILL_DIR も同じ fail-closed 方針を取り、${ORIG_DIR}/skills/contribute-skill・${ORIG_DIR}/.agents/skills/contribute-skill・${ORIG_DIR}/.claude/skills/contribute-skill の3候補のみ受理する(末尾要素・中間の親ディレクトリのいずれかが symlink なら実体側パスの指定を要求)。3候補のうち複数が存在する場合は silently にどれかを優先せず中止して環境変数 CONTRIBUTE_SKILL_DIR での指定を求める(LOCAL_SKILL_DIR とは非対称にしない)
- source が Fandhe-AI org 以外の場合は中止:前方一致(
Fandhe-AI/* 等)ではなく、正規化(.git 除去等)後の OWNER/REPO が ^Fandhe-AI/[A-Za-z0-9._-]+$ に完全一致するかで判定する。../ によるパストラバーサル・クエリ・フラグメント・余剰パスセグメントを含む値、および repo 名が ./.. になる値は中止し、意図しない外部リポジトリへの push を防ぐ
- セキュリティ問題が見つかった場合は中止:修正後に再実行
- upstream の配置はクローンしたリポジトリのレイアウトで判定する:
skills-lock.json の skillPath はローカル install パス(例: .agents/skills/github-docs/SKILL.md)であり、upstream リポジトリ内の配置ではない。skillPath の dirname を UPSTREAM_SKILL_PATH に採用してはならない。判定順は skills/<name> の存在 → .agents/skills/<name> の存在 → .claude/skills/<name> の存在 → スキルルート親ディレクトリの慣習(skills/ → .agents/skills/ → .claude/skills/ の順。新規スキルは個別パスが存在しないためこの親ディレクトリ判定で配置先が決まる)→ 最終デフォルト skills/(より一般的な公開レイアウト)。全候補で assert_no_symlink_components により経路の全要素(.claude 等の最上位親を含む)が symlink でない場合のみ採用し、.claude 自体がリポジトリ外を指す symlink でも外部内容が upstream へコピーされない(fail-closed。symlink 経由の実体は前段の実体側候補で検出される)
- 宛先は消してからコピーする(削除伝搬): は追加・上書きのみで削除を反映しないため、ローカルで削除したファイルが upstream 側に残存してしまう。 前に が ・・ のいずれかであることを case 文で検証し、それ以外の値なら中止する。加えて rm -rf 直前に実体パス(symlink 境界・clone ルート配下チェック、cd -P + 相対 rm による TOCTOU 対策)を再検証する。削除対象は必ず clone 用の一時ディレクトリ()配下のみに閉じ、それ以外のファイルには一切触れない。
sandbox 環境での実行
このスキルはネットワーク越しの GitHub 操作(fork・git push・PR 作成)を必須とする。該当コマンドはコマンド単位で sandbox 無効にして実行する。ネットワーク遮断を解除できない環境では実行できない。
検証
PR 作成後、以下で完了を確認する。
gh pr view --repo "${REPO_SLUG}" --web
- PR URL が返されること
- PR のタイトル・差分が意図した内容であること
sync-skills-lock 実行案内が出力されていること
既存スキルとの関係
- Step 4 のセキュリティチェック、Step 7 の Conventional Commits、Step 8 の PR body は
create-pr/SKILL.md の流儀を踏襲
- マージ後は
sync-skills-lock で skills-lock.json の computedHash を更新