diff --git a/plugins/git-pilot/CLAUDE.md b/plugins/git-pilot/CLAUDE.md index a5111ef..e6dd549 100644 --- a/plugins/git-pilot/CLAUDE.md +++ b/plugins/git-pilot/CLAUDE.md @@ -1,3 +1,5 @@ +> **Note:** The `git-pilot-workflow` skill contains the definitive behavioral contract for git-pilot. This CLAUDE.md file serves as reinforcement. If both are loaded, the skill takes precedence. + # git-pilot — Git Workflow Autopilot git-pilot manages the full git workflow lifecycle. You MUST follow these rules throughout every session. diff --git a/plugins/git-pilot/scripts/post-bash.sh b/plugins/git-pilot/scripts/post-bash.sh index 2783105..dfbd818 100755 --- a/plugins/git-pilot/scripts/post-bash.sh +++ b/plugins/git-pilot/scripts/post-bash.sh @@ -69,7 +69,7 @@ if echo "$command" | grep -qE 'git\s+commit'; then unpushed_count=$(echo "$unpushed" | wc -l | tr -d ' ') # Emit push prompt — CLAUDE.md instructs Claude to act on this with AskUserQuestion - message="[git-pilot] ${unpushed_count} unpushed commit(s) on '${current_branch}'. Push command: git push -u ${remote_name} ${current_branch}" + message="[git-pilot] ${unpushed_count} unpushed commit(s) on '${current_branch}'. You MUST use AskUserQuestion NOW to ask the user whether to push. Push command: git push -u ${remote_name} ${current_branch}. Do not silently skip this prompt." jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}' exit 0 @@ -88,11 +88,12 @@ if echo "$command" | grep -qE 'git\s+push'; then if echo "$stderr" | grep -qiE 'rejected|failed to push|non-fast-forward'; then current_branch=$(get_current_branch) remote_name=$(get_config "$config" '.remote.defaultName' 'origin') - message="[git-pilot] Push rejected -- remote '${remote_name}/${current_branch}' has new commits. Prompt the user: + message="[git-pilot] Push rejected — remote '${remote_name}/${current_branch}' has new commits. STOP and use AskUserQuestion NOW to present these options: 1. Pull and rebase, then retry push (git pull --rebase && git push) 2. Force push with lease (git push --force-with-lease) 3. Pull and merge (git pull) -4. Cancel" +4. Cancel +Do not attempt any resolution without the user's choice." jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}' exit 0 fi diff --git a/plugins/git-pilot/scripts/post-write.sh b/plugins/git-pilot/scripts/post-write.sh index 6900b63..1004dd7 100755 --- a/plugins/git-pilot/scripts/post-write.sh +++ b/plugins/git-pilot/scripts/post-write.sh @@ -73,10 +73,10 @@ write_state "$state_file" "$reset_state" # Act based on mode case "$mode" in suggest) - message="[git-pilot] You've made ${threshold} file changes since the last commit. Consider committing your progress for easier rollback. Modified files: ${modified_files}" + message="[git-pilot] You have made ${threshold} file changes since the last commit (${modified_files}). You MUST commit your progress now for easier rollback. Use AskUserQuestion to confirm with the user, then commit with a descriptive message following the configured commit format." ;; auto) - message="[git-pilot] Auto-commit threshold reached. Commit your current changes now with a descriptive message following the commit format: ${commit_pattern}" + message="[git-pilot] Auto-commit threshold reached (${threshold} file changes). You MUST commit your current changes NOW with a descriptive message following the commit format: ${commit_pattern}. Do not continue writing code until this commit is made." ;; silent) commit_msg="${wip_prefix}checkpoint after ${threshold} file changes" @@ -90,7 +90,7 @@ case "$mode" in ;; *) # Unknown mode, fall back to suggest - message="[git-pilot] You've made ${threshold} file changes since the last commit. Consider committing your progress for easier rollback. Modified files: ${modified_files}" + message="[git-pilot] You have made ${threshold} file changes since the last commit (${modified_files}). You MUST commit your progress now for easier rollback. Use AskUserQuestion to confirm with the user, then commit with a descriptive message following the configured commit format." ;; esac diff --git a/plugins/git-pilot/scripts/prompt-context.sh b/plugins/git-pilot/scripts/prompt-context.sh index da60932..c1f0bd8 100755 --- a/plugins/git-pilot/scripts/prompt-context.sh +++ b/plugins/git-pilot/scripts/prompt-context.sh @@ -38,5 +38,5 @@ if [[ "$commit_count" == "0" ]]; then echo '{"continue": true}'; exit 0; fi branch_purpose=$(derive_branch_purpose "$current_branch") recent_commits=$(git log "${default_branch}..${current_branch}" --oneline --no-decorate -5 2>/dev/null || true) -message="[git-pilot] Branch context: '${current_branch}' (${branch_purpose}). ${commit_count} commit(s). Recent: ${recent_commits}" +message="[git-pilot] You are on branch '${current_branch}' (purpose: ${branch_purpose}, ${commit_count} commit(s)). Before acting on the user's next request, assess whether the request is related to this branch's purpose. If unrelated, STOP and use AskUserQuestion to suggest creating a new branch. Recent commits: ${recent_commits}" jq -n --arg msg "$message" '{"continue": true, "systemMessage": $msg}' diff --git a/plugins/git-pilot/scripts/session-start.sh b/plugins/git-pilot/scripts/session-start.sh index 86c8400..96a0c08 100755 --- a/plugins/git-pilot/scripts/session-start.sh +++ b/plugins/git-pilot/scripts/session-start.sh @@ -78,9 +78,9 @@ if is_git_repo; then | grep -m1 'checkout: moving from' \ | sed 's/checkout: moving from \([^ ]*\) to .*/\1/' || true) if [[ -n "$prev_branch" ]]; then - messages+=("[git-pilot] Detached HEAD at ${head_sha}. Previous branch was '${prev_branch}'. Prompt the user: return to '${prev_branch}', create a new branch from HEAD, or continue in detached state.") + messages+=("[git-pilot] Detached HEAD at ${head_sha}. You MUST NOT make any code changes in detached HEAD state. Use AskUserQuestion NOW to ask the user to choose: (1) return to '${prev_branch}', (2) create a new branch from HEAD, or (3) continue in detached state. Do not proceed until the user responds.") else - messages+=("[git-pilot] Detached HEAD at ${head_sha}. Prompt the user: create a new branch from HEAD or continue in detached state.") + messages+=("[git-pilot] Detached HEAD at ${head_sha}. You MUST NOT make any code changes in detached HEAD state. Use AskUserQuestion NOW to ask the user to choose: (1) create a new branch from HEAD, or (2) continue in detached state. Do not proceed until the user responds.") fi fi @@ -94,16 +94,16 @@ if is_git_repo; then if git merge --ff-only "${remote_name}/${current_branch}" >/dev/null 2>&1; then messages+=("[git-pilot] Branch '${current_branch}' was ${behind_count} commit(s) behind '${remote_name}/${current_branch}'. Fast-forwarded to latest.") else - messages+=("[git-pilot] Branch '${current_branch}' is ${behind_count} commit(s) behind '${remote_name}/${current_branch}' but fast-forward failed. Prompt the user: pull with merge, reset to remote, or continue as-is.") + messages+=("[git-pilot] Branch '${current_branch}' is ${behind_count} commit(s) behind '${remote_name}/${current_branch}' and fast-forward failed. You MUST use AskUserQuestion NOW to ask the user to choose: (1) pull with merge, (2) reset to remote, or (3) continue as-is. Do not proceed with any code changes until this is resolved.") fi ;; diverged:*:*) IFS=':' read -r _ ahead_count behind_count <<< "$tracking_status" - messages+=("[git-pilot] Branch '${current_branch}' has diverged from '${remote_name}/${current_branch}' (${ahead_count} local, ${behind_count} remote). Prompt the user: rebase onto remote, merge remote, reset to remote, or continue.") + messages+=("[git-pilot] Branch '${current_branch}' has diverged from '${remote_name}/${current_branch}' (${ahead_count} local, ${behind_count} remote). You MUST use AskUserQuestion NOW to ask the user to choose: (1) rebase onto remote, (2) merge remote changes, (3) reset to remote, or (4) continue as-is. Do not make any code changes until this is resolved.") ;; ahead:*) ahead_count="${tracking_status#ahead:}" - messages+=("[git-pilot] Branch '${current_branch}' is ${ahead_count} commit(s) ahead of '${remote_name}/${current_branch}'. Unpushed changes.") + messages+=("[git-pilot] Branch '${current_branch}' has ${ahead_count} unpushed commit(s). Before continuing, use AskUserQuestion to ask the user whether to push now or continue working.") ;; # up-to-date and no-remote: no message esac @@ -117,9 +117,9 @@ if is_git_repo; then branch_types=$(echo "$CONFIG" | jq -r '.branch.types // ["feat","fix","refactor","docs","test","chore","style","perf","build","ci"] | join(", ")') if has_uncommitted_changes; then - messages+=("[git-pilot] On '${default_branch}' with uncommitted changes. Prompt the user: stash and create branch, commit first, or continue on '${default_branch}'.") + messages+=("[git-pilot] You are on the default branch '${default_branch}' with uncommitted changes. You MUST NOT make any further code changes until this is resolved. Use AskUserQuestion NOW to ask the user to choose: (1) stash changes and create a feature branch, (2) commit current changes first, or (3) continue on '${default_branch}'. Do not proceed until the user responds.") else - messages+=("[git-pilot] On default branch '${default_branch}'. Prompt the user to create a branch before making changes. Pattern: ${branch_pattern}, types: ${branch_types}.") + messages+=("[git-pilot] You are on the default branch '${default_branch}'. You MUST NOT make any code changes until a feature branch is created. Use AskUserQuestion to ask the user for the branch type and description NOW, before doing anything else. Pattern: ${branch_pattern}, types: ${branch_types}.") fi fi fi @@ -131,7 +131,7 @@ if is_git_repo; then skip_remote=$(get_config "$CONFIG" '.remote.skipRemotePrompt' 'false') if [[ "$prompt_remote" == "true" ]] && [[ "$skip_remote" == "false" ]]; then - messages+=("[git-pilot] No git remote configured. Prompt the user to add one or skip.") + messages+=("[git-pilot] No git remote configured. Use AskUserQuestion to ask the user whether to add a remote now (provide the command format: git remote add origin ) or skip. This affects push and sync capabilities for this session.") fi fi fi diff --git a/plugins/git-pilot/scripts/session-stop.sh b/plugins/git-pilot/scripts/session-stop.sh index e355f0c..661ac63 100755 --- a/plugins/git-pilot/scripts/session-stop.sh +++ b/plugins/git-pilot/scripts/session-stop.sh @@ -171,7 +171,7 @@ if [[ "$session_had_changes" == "true" ]] && has_remote; then case "$drift_status" in drifted:*) drift_count="${drift_status#drifted:}" - messages+=("[git-pilot] Base branch '${default_branch}' has ${drift_count} new commit(s). Rebasing '${current_branch}' onto '${remote_name}/${default_branch}'...") + messages+=("[git-pilot] Base branch '${default_branch}' has ${drift_count} new commit(s) since you branched. Rebasing '${current_branch}' onto '${remote_name}/${default_branch}' now. If conflicts arise, you MUST use AskUserQuestion to present resolution options before proceeding.") rebase_result=$(attempt_rebase "${remote_name}/${default_branch}" || true) case "$rebase_result" in success) @@ -184,7 +184,7 @@ if [[ "$session_had_changes" == "true" ]] && has_remote; then conflicts=$(get_conflict_details) conflict_count=$(echo "$conflicts" | jq 'length') conflict_files=$(echo "$conflicts" | jq -r '.[].file' | paste -sd', ') - messages+=("[git-pilot] Rebase conflicts in ${conflict_count} file(s): ${conflict_files}. Prompt the user to resolve conflicts, abort rebase, or use merge instead.") + messages+=("[git-pilot] Rebase conflicts in ${conflict_count} file(s): ${conflict_files}. STOP and use AskUserQuestion NOW to ask the user to choose: (1) resolve conflicts manually, (2) abort rebase, or (3) fall back to merge. Do not proceed until the user responds.") ;; abort) git rebase --abort 2>/dev/null || true @@ -197,7 +197,7 @@ if [[ "$session_had_changes" == "true" ]] && has_remote; then else fallback_conflicts=$(get_conflict_details) fallback_conflict_count=$(echo "$fallback_conflicts" | jq 'length') - messages+=("[git-pilot] Both rebase and merge have conflicts in ${fallback_conflict_count} file(s). Prompt the user to resolve.") + messages+=("[git-pilot] Both rebase and merge have conflicts in ${fallback_conflict_count} file(s). STOP and use AskUserQuestion NOW to present the conflict details and ask the user how to proceed. Do not push or continue until conflicts are resolved.") fi ;; esac @@ -208,7 +208,7 @@ if [[ "$session_had_changes" == "true" ]] && has_remote; then # No action needed ;; no-common-ancestor) - messages+=("[git-pilot] Cannot determine common ancestor between '${current_branch}' and '${default_branch}'. Skipping rebase. Push may require manual review.") + messages+=("[git-pilot] Cannot determine common ancestor between '${current_branch}' and '${default_branch}'. Skipping rebase. Before pushing, use AskUserQuestion to warn the user that the push may require manual review.") ;; esac fi @@ -235,7 +235,7 @@ if [[ "$session_had_changes" == "true" ]] && has_remote; then case "$push_on_finish" in always) - messages+=("[git-pilot] Pushing ${unpushed_count} commit(s) to '${remote_name}/${current_branch}'. Run: git push -u ${remote_name} ${current_branch}") + messages+=("[git-pilot] You MUST push ${unpushed_count} commit(s) to '${remote_name}/${current_branch}' now. Run: git push -u ${remote_name} ${current_branch}. Execute this command immediately.") ;; *) # "ask" and "never" are silent in the stop hook. @@ -354,7 +354,7 @@ if [[ "$session_had_changes" == "true" ]] && has_remote; then fi if [[ -n "$mr_cmd" ]]; then - messages+=("[git-pilot] Creating merge/pull request. Run: ${mr_cmd}") + messages+=("[git-pilot] You MUST create a merge/pull request now. Run: ${mr_cmd}. Execute this command immediately.") fi fi # If CLI tool is missing, stay silent — user can use /finish or create MR manually @@ -386,7 +386,7 @@ if [[ "$active_count" -gt 0 ]]; then fi remaining=$(list_worktrees | jq '.worktrees | length') if [[ "$remaining" -gt 0 ]]; then - messages+=("[git-pilot] ${remaining} active worktree(s) remain. Use /worktree to manage them.") + messages+=("[git-pilot] ${remaining} active worktree(s) remain after session cleanup. Before ending, use AskUserQuestion to ask the user whether to clean up remaining worktrees using the /worktree skill.") fi fi diff --git a/plugins/git-pilot/skills/branch/SKILL.md b/plugins/git-pilot/skills/branch/SKILL.md index 18d7f38..131e1db 100644 --- a/plugins/git-pilot/skills/branch/SKILL.md +++ b/plugins/git-pilot/skills/branch/SKILL.md @@ -1,6 +1,7 @@ --- name: branch -description: Create a new branch using the configured naming pattern +user-invocable: true +description: "USE THIS SKILL WHEN you are about to start making code changes and you are on the default branch, when the user describes a new feature or fix, or when starting any implementation work. Creates a new branch using the configured naming pattern so work never happens directly on the default branch. Trigger on: user requests a feature, bug fix, refactor, or any code change; user says 'let's work on X'; you detect you are on main/master/default branch before writing code; the git-pilot-workflow skill directs you to branch. Even if the user doesn't explicitly ask for a branch, still use this skill before making changes on the default branch." --- # /branch diff --git a/plugins/git-pilot/skills/configure/SKILL.md b/plugins/git-pilot/skills/configure/SKILL.md index b2910c8..06d5bc1 100644 --- a/plugins/git-pilot/skills/configure/SKILL.md +++ b/plugins/git-pilot/skills/configure/SKILL.md @@ -1,6 +1,7 @@ --- name: configure -description: Configure git-pilot settings using natural language +description: "USE THIS SKILL WHEN the user wants to change any git-pilot behavior, mentions commit format preferences, branch naming conventions, or push/merge request settings. Configures git-pilot settings through natural language. Trigger on phrases like 'stop asking me about X,' 'always do Y,' 'change the commit format,' 'use underscores in branch names,' 'auto-push after commit,' 'disable auto-commit,' or any request to customize git workflow behavior. Even if the user only vaguely mentions wanting something to work differently, still use this skill." +user-invocable: true --- # /configure diff --git a/plugins/git-pilot/skills/finish/SKILL.md b/plugins/git-pilot/skills/finish/SKILL.md index 0444e80..642e42a 100644 --- a/plugins/git-pilot/skills/finish/SKILL.md +++ b/plugins/git-pilot/skills/finish/SKILL.md @@ -1,6 +1,7 @@ --- name: finish -description: Finish work - commit remaining changes, push, and optionally create a merge request +user-invocable: true +description: "USE THIS SKILL WHEN the user signals that the current unit of work is over. Commits remaining changes, pushes to remote, and optionally creates a merge request to wrap up the session. Trigger on: user says 'done', 'finished', 'that's it', 'ship it', 'wrap it up', 'we're good', 'let's call it', 'I'm done', 'push it', end of session, task completion, or any indication that work is complete. Even if the user only hints at being done or says something casual like 'looks good', still use this skill to ensure nothing is left uncommitted or unpushed." --- # /finish diff --git a/plugins/git-pilot/skills/git-pilot-workflow/SKILL.md b/plugins/git-pilot/skills/git-pilot-workflow/SKILL.md new file mode 100644 index 0000000..c17aa3a --- /dev/null +++ b/plugins/git-pilot/skills/git-pilot-workflow/SKILL.md @@ -0,0 +1,164 @@ +--- +name: git-pilot-workflow +description: "USE THIS SKILL WHEN the user asks to write code, fix a bug, implement a feature, refactor, add tests, update documentation, or do any work that results in file changes in a git repository. This skill governs the git workflow throughout the entire session -- branch creation, commit formatting, push prompts, and session-end procedures. Trigger on any coding or development task. Even if the user does not mention git, if the task involves code changes, still use this skill." +--- + +# git-pilot Workflow + +This skill is the definitive behavioral contract for git-pilot. It governs the +full git workflow lifecycle: branching, committing, pushing, session end, and +hook compliance. Follow these rules throughout every session where file changes +occur in a git repository. + +--- + +## 1. Branch Discipline + +**Never work directly on the default branch.** Before making any changes, ensure +you are on a feature branch. If you are on the default branch, create a new +branch first. + +### Branch Naming Convention + +Use the configured pattern (default: `{{type}}/{{description}}`). + +- Format the description in kebab-case (e.g., `feat/add-user-auth`). +- Available types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `style`, + `perf`, `build`, `ci`. +- If the user's request clearly implies a type and description, infer the branch + name and propose it. Otherwise, ask the user. + +### Configuration + +Check `.claude/git-pilot.json` (local) or `~/.claude/git-pilot.json` (global) +for project-specific overrides to the branch pattern, types, separator, case, +and max length. + +> For the full branch creation procedure, see the `/branch` skill. + +--- + +## 2. Commit Discipline + +Follow the configured commit message format (default: +`{{type}}({{scope}}): {{description}}`). + +### Rules + +- **No AI attribution.** Do NOT include `Co-Authored-By`, `Generated with`, or + any AI attribution lines in commit messages. +- **Subject-only by default.** Check `commit.body.required` in the effective + config. If `false`, commits MUST be subject-line only -- no body. + - **Exception -- breaking changes:** When the subject contains `!:` and + `commit.breakingChange.requireBody` is `true`, include a body starting with + the configured `bodyPrefix` (default: `BREAKING CHANGE: `). +- **Max length.** Keep subjects under the configured max length (default: 72). +- **Imperative mood.** Use "add" not "added", "fix" not "fixed". +- **One logical change per commit.** When you complete a coherent unit of work, + commit it. Do not accumulate unrelated changes. + +--- + +## 3. Push Workflow + +After every successful `git commit`, check for unpushed commits. If there are +unpushed commits and a remote exists: + +1. Use **AskUserQuestion** to prompt the user with two options: + - **"Push to ``/``"** + - **"Skip"** +2. If the user chooses to push, run: `git push -u `. + +**Do NOT silently skip this step** or just mention it in passing. The user +expects an interactive prompt after each commit. + +--- + +## 4. Session End + +When the user indicates work is done, or the session is ending, follow the +`/finish` skill workflow: + +1. Commit any remaining uncommitted changes (following the commit rules above). +2. If unpushed commits exist, prompt to push (same as Section 3). +3. If merge request creation is enabled in config, offer to create one using the + appropriate platform CLI (`gh` for GitHub, `glab` for GitLab). Skip silently + if the CLI is not available. + +> For the full session-end procedure, see the `/finish` skill. + +--- + +## 5. Hook Compliance + +When you receive a system message prefixed with `[git-pilot]` from any hook +(SessionStart, PostToolUse, Stop): + +- **Act on it immediately** using AskUserQuestion BEFORE continuing with other + work. +- **Never ignore** hook messages. Present clear, concise options relevant to the + prompt. + +These messages are generated by git-pilot hooks and require your attention +before proceeding with any other task. + +--- + +## 6. Unrelated Work Detection + +Before starting work on a new user request, assess whether the request belongs +on the current branch: + +1. **Parse the branch name** for semantic meaning. For example, + `feat/add-dark-mode` implies dark mode work; `fix/login-timeout` implies a + login timeout fix. +2. **Review recent commits** on this branch for scope context. +3. **If the request is clearly unrelated** to the branch's purpose (different + feature, different bug, different module), prompt the user with options: + - Create a new branch from `` (recommended -- keeps branches + focused) + - Create a new branch from the current branch (if the new work depends on + current changes) + - Continue on this branch + +### Branch Switching on New Branch + +If the user chooses to create a new branch (option 1 or 2 above), follow the +branch switch workflow: + +- If there are uncommitted changes and `branch.autoStashOnSwitch` is `true` in + the effective config, stash them automatically before switching. Inform the + user: "Stashed changes on ''." +- After switching to the target branch, check if there is a git-pilot stash for + that branch and restore it automatically. +- If stash restoration fails due to conflicts, inform the user and suggest + manual resolution. + +### When NOT to Prompt + +Do NOT prompt for unrelated work detection when: + +- The work is closely related (e.g., fixing a bug discovered while implementing + a feature on the same branch). +- You are on the default branch. +- The current branch has no commits yet. + +--- + +## 7. Cross-References + +This skill provides the high-level behavioral contract. For detailed procedures, +consult the following sub-skills: + +| Skill | Purpose | +|-------|---------| +| `/branch` | Branch creation and switching | +| `/finish` | Session end workflow (commit, push, MR) | +| `/summary` | Branch work recap | +| `/configure` | Settings management | +| `/stash` | Stash management | +| `/worktree` | Worktree management | +| `/rebase` | Rebasing | + +Do NOT duplicate the full content of these sub-skills. Use them for the detailed +step-by-step procedures; this skill defines when and why to invoke them. diff --git a/plugins/git-pilot/skills/rebase/SKILL.md b/plugins/git-pilot/skills/rebase/SKILL.md index ec523c0..ed37898 100644 --- a/plugins/git-pilot/skills/rebase/SKILL.md +++ b/plugins/git-pilot/skills/rebase/SKILL.md @@ -1,6 +1,7 @@ --- name: rebase -description: Rebase current branch onto another branch +description: "USE THIS SKILL WHEN the user mentions rebasing, the current branch is behind the base branch, before pushing on long-lived feature branches, or when merge conflicts from diverged history need resolution. Rebases the current branch onto a target branch with conflict handling and force-push management. Trigger on 'rebase,' 'update my branch,' 'catch up with main,' 'branch is behind,' 'linearize history,' 'clean up commits before push,' or any signal that the branch needs to incorporate upstream changes. Even if the user just says 'sync with main' or 'get latest changes,' still use this skill." +user-invocable: true --- # /rebase diff --git a/plugins/git-pilot/skills/stash/SKILL.md b/plugins/git-pilot/skills/stash/SKILL.md index 69250ed..549f785 100644 --- a/plugins/git-pilot/skills/stash/SKILL.md +++ b/plugins/git-pilot/skills/stash/SKILL.md @@ -1,6 +1,7 @@ --- name: stash -description: Manage git stashes - save, list, apply, or drop +description: "USE THIS SKILL WHEN the user wants to set aside current work, switch context temporarily, save progress without committing, or needs to switch branches with uncommitted changes. Manages git stashes including save, list, apply, pop, and drop operations. Trigger on 'stash my changes,' 'save this for later,' 'set aside,' 'park this work,' 'switch branches' with dirty working tree, 'come back to this later,' or any need to temporarily shelve uncommitted changes. Even if the user just says 'hold on to this' or 'I need to context-switch,' still use this skill." +user-invocable: true --- # /stash diff --git a/plugins/git-pilot/skills/summary/SKILL.md b/plugins/git-pilot/skills/summary/SKILL.md index a951b57..83e3931 100644 --- a/plugins/git-pilot/skills/summary/SKILL.md +++ b/plugins/git-pilot/skills/summary/SKILL.md @@ -1,6 +1,7 @@ --- name: summary -description: Show a summary of work done on the current branch +user-invocable: true +description: "USE THIS SKILL WHEN the user asks for a recap of work done, wants to know what changed, or needs a progress report. Shows a summary of commits and file changes on the current branch. Trigger on: user says 'what did we do', 'what changed', 'summarize', 'recap', 'show me what we did', 'progress report', 'status update', session ending where a summary would be useful, or any request to review the work performed on this branch. Even if the user only vaguely asks about recent changes or progress, still use this skill to provide a clear overview." --- # /summary diff --git a/plugins/git-pilot/skills/worktree/SKILL.md b/plugins/git-pilot/skills/worktree/SKILL.md index 280b5dc..a6436d4 100644 --- a/plugins/git-pilot/skills/worktree/SKILL.md +++ b/plugins/git-pilot/skills/worktree/SKILL.md @@ -1,6 +1,7 @@ --- name: worktree -description: Manage git worktrees for parallel branch work +description: "USE THIS SKILL WHEN the user needs to work on multiple branches simultaneously, wants parallel development across features, or needs isolation between tasks without stashing. Manages git worktrees for creating, listing, removing, and merging parallel branch checkouts. Trigger on 'work on two things at once,' 'parallel branches,' 'separate checkout,' 'isolate this work,' 'worktree,' 'don't want to stash,' or any request for concurrent branch work. Even if the user just wants to quickly check another branch without losing context, still use this skill." +user-invocable: true --- # /worktree diff --git a/plugins/opaq/CLAUDE.md b/plugins/opaq/CLAUDE.md new file mode 100644 index 0000000..0bf48ed --- /dev/null +++ b/plugins/opaq/CLAUDE.md @@ -0,0 +1,27 @@ +# opaq — Behavioral Rules + +These rules apply whenever the opaq plugin is installed. They govern how you interact with credentials and secrets throughout the session. + +## Rule 1: Check opaq before asking the user for credentials + +If a task requires an API token, password, SSH key, registry credential, or any secret not already in the environment, search opaq first. Do not ask the user to provide the credential directly unless opaq has no matching secret. + +## Rule 2: Always use the `opaq run --` wrapper + +Never attempt to inline, echo, or extract secret values. Always use placeholder syntax `{{SECRET_NAME}}` inside `opaq run --` commands. + +## Rule 3: Act on authentication failures + +If a command fails with a 401, 403, or "authentication required" error, and no credentials were provided, search opaq for relevant secrets before retrying. + +## Rule 4: Never write opaq secrets to files + +If a configuration file needs credentials managed by opaq, tell the user to fill them in manually. Do not write `{{SECRET_NAME}}` placeholders into config files, `.env` files, scripts, or any persisted file. + +## Rule 5: Treat `[MASKED]` output as expected + +When opaq runs a command, secret values in stdout/stderr are replaced with `[MASKED]`. This is normal. Do not attempt to recover, decode, or reconstruct masked values. + +## Skill Reference + +For the detailed credential workflow (search, run, security rules), read the `using-opaq` skill. diff --git a/plugins/opaq/hooks/hooks.json b/plugins/opaq/hooks/hooks.json index 42efca8..f7b21f3 100644 --- a/plugins/opaq/hooks/hooks.json +++ b/plugins/opaq/hooks/hooks.json @@ -21,13 +21,26 @@ ] } ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/post-bash-auth-check.sh", + "timeout": 5 + } + ] + } + ], "SessionStart": [ { "matcher": "startup|compact", "hooks": [ { "type": "command", - "command": "if command -v opaq >/dev/null 2>&1; then echo 'opaq is available for secure credential access. When a task needs credentials not already in your environment or .env files, use: `opaq search ` to find secrets, then `opaq run -- ` with {{SECRET_NAME}} placeholders. Secrets managed by opaq must never be written to files.'; else echo 'WARNING: opaq is not installed. Credential management via opaq is unavailable. Install it first: https://github.com/moukrea/opaq'; fi" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-start.sh", + "timeout": 5 } ] } diff --git a/plugins/opaq/scripts/post-bash-auth-check.sh b/plugins/opaq/scripts/post-bash-auth-check.sh new file mode 100755 index 0000000..7fe1936 --- /dev/null +++ b/plugins/opaq/scripts/post-bash-auth-check.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# post-bash-auth-check.sh — PostToolUse hook for Bash commands +# +# Detects authentication failures in command output and suggests +# using opaq to find and apply credentials. + +set -euo pipefail + +# Only act if opaq is available +command -v opaq >/dev/null 2>&1 || exit 0 + +INPUT=$(cat) + +# Extract the tool result (stdout/stderr) and the original command +TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_result.stdout // empty') +TOOL_STDERR=$(echo "$INPUT" | jq -r '.tool_result.stderr // empty') +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +# shellcheck disable=SC2034 # extracted for potential future use +EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_result.exit_code // "0"') + +# Combine stdout and stderr for pattern matching +COMBINED="${TOOL_RESULT} +${TOOL_STDERR}" + +[ -z "$COMBINED" ] && exit 0 + +# Check for authentication failure patterns (case-insensitive) +AUTH_FAILURE=false + +if echo "$COMBINED" | grep -qiE '(HTTP[/ ](401|403)\b|status[: ]+(401|403)\b)'; then + AUTH_FAILURE=true +elif echo "$COMBINED" | grep -qiE 'authentication required|unauthorized|access denied|permission denied|invalid token|bad credentials|login required'; then + AUTH_FAILURE=true +elif echo "$COMBINED" | grep -qiE 'denied: requested access to the resource is denied'; then + AUTH_FAILURE=true +elif echo "$COMBINED" | grep -qiE 'Authentication failed|could not read Username'; then + AUTH_FAILURE=true +fi + +[ "$AUTH_FAILURE" = "false" ] && exit 0 + +# Infer a keyword from the command +KEYWORD="credentials" + +if echo "$COMMAND" | grep -qiE 'docker|registry'; then + KEYWORD="docker" +elif echo "$COMMAND" | grep -qiE '^\s*git\b|\.git'; then + KEYWORD="git" +elif echo "$COMMAND" | grep -qiE '\bssh\b'; then + KEYWORD="ssh" +elif echo "$COMMAND" | grep -qiE '\bnpm\b'; then + KEYWORD="npm" +elif echo "$COMMAND" | grep -qoE 'https?://[^/ ]+' | head -1 | grep -qoE '[^.]+\.[^.]+$'; then + # Extract domain from URL — e.g., api.github.com -> github + KEYWORD=$(echo "$COMMAND" | grep -oE 'https?://[^/ ]+' | head -1 | sed 's|https\?://||' | awk -F. '{if (NF>=2) print $(NF-1); else print $1}') +fi + +jq -n --arg keyword "$KEYWORD" '{ + hookSpecificOutput: { + hookEventName: "PostToolUse", + systemMessage: ("[opaq] Authentication failure detected. Search opaq for relevant credentials: `opaq search " + $keyword + "`. Then retry the command using `opaq run -- ` with the appropriate {{SECRET_NAME}} placeholder.") + } +}' diff --git a/plugins/opaq/scripts/session-start.sh b/plugins/opaq/scripts/session-start.sh new file mode 100755 index 0000000..930e809 --- /dev/null +++ b/plugins/opaq/scripts/session-start.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# session-start.sh — SessionStart hook for the opaq plugin +# +# Checks opaq availability and emits a directive system message +# telling the agent how to use opaq for credential management. + +set -euo pipefail + +# Check if opaq is installed and available +if ! command -v opaq >/dev/null 2>&1; then + jq -n '{ + hookSpecificOutput: { + hookEventName: "SessionStart", + systemMessage: "[opaq] WARNING: opaq is not installed. Credential management via opaq is unavailable. If a task requires secrets, you will need to ask the user directly. Install opaq: https://github.com/moukrea/opaq" + } + }' + exit 0 +fi + +# Check if opaq store is initialized (search should not error) +if ! opaq search __ping__ >/dev/null 2>&1; then + jq -n '{ + hookSpecificOutput: { + hookEventName: "SessionStart", + systemMessage: "[opaq] WARNING: opaq is installed but the store may not be initialized or is locked. Credential management may be unavailable. The user may need to run `opaq init` or `opaq unlock`." + } + }' + exit 0 +fi + +jq -n '{ + hookSpecificOutput: { + hookEventName: "SessionStart", + systemMessage: "[opaq] Secure credential manager is active. When any task requires credentials (API tokens, passwords, SSH keys, registry logins, deployment secrets), you MUST use `opaq search ` to find them and `opaq run -- ` with {{PLACEHOLDER}} syntax to use them. Do NOT ask the user for secrets or hardcode credentials without checking opaq first." + } +}' diff --git a/plugins/opaq/skills/using-opaq/SKILL.md b/plugins/opaq/skills/using-opaq/SKILL.md index e211ad5..7609a1b 100644 --- a/plugins/opaq/skills/using-opaq/SKILL.md +++ b/plugins/opaq/skills/using-opaq/SKILL.md @@ -1,6 +1,6 @@ --- name: using-opaq -description: Securely access credentials and secrets for CLI commands using the opaq tool. Use this skill when a task requires authentication credentials (API tokens, passwords, SSH keys, registry logins) that are NOT already available in environment variables, .env files, or provided directly by the user. Triggers include API calls needing auth headers, CI/CD pipeline access, docker/registry logins, SSH connections, or any command referencing tokens, secrets, or credentials not present in the current environment. Do NOT use this skill for credentials the user has explicitly provided in the conversation or that exist in environment variables. +description: "USE THIS SKILL WHEN any task requires authentication, API tokens, passwords, SSH keys, registry logins, deployment credentials, CI/CD secrets, database passwords, or any command that needs a secret the user has not explicitly provided. This skill securely retrieves and injects credentials via the opaq secret manager. Trigger on: API calls needing auth headers, docker/registry logins, SSH connections, git operations requiring credentials, CI/CD pipeline access, database connections, cloud provider auth, or any command referencing tokens, secrets, or credentials not present in the current environment. If a command fails with a 401, 403, or any authentication error, ALWAYS consult this skill before retrying. Before hardcoding any credential or asking the user for a secret, ALWAYS check this skill first. Even if you think the credential might not be in opaq, still use this skill to search before falling back to asking the user." --- # Using opaq