Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@
"author": {
"name": "JacobPEvans"
}
},
{
"name": "skill-guards",
"source": "./skill-guards",
"description": "Ensures fresh execution of skills on every invocation via UserPromptSubmit hook",
"version": "2.3.2",
"author": {
"name": "JacobPEvans"
}
}
]
}
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ This is a **Claude Code plugins repository** containing production-ready hooks f
| **process-cleanup** | PostToolUse | — | Cleanup orphaned MCP server processes on session exit |
| **project-standards** | Skill | `/agentsmd-authoring`, `/workspace-standards`, `/skills-registry` | AgentsMD authoring standards, workspace management, and skills/tools registry lookup |
| **session-analytics** | Skill | `/token-breakdown` | Session token analytics via Splunk OTEL telemetry |
| **skill-guards** | UserPromptSubmit | — | Ensures fresh execution of skills on every invocation |

## Multi-Model Delegation

Expand Down
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"ENDJSON",
"RLENGTH",
"unrecognised",
"tokenisation"
"tokenisation",
"reinvocation"
]
}
3 changes: 3 additions & 0 deletions git-workflows/skills/refresh-repo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ description: Check PR merge readiness, sync local repo, and cleanup stale worktr
Check open PR merge-readiness status, sync the local repository, and cleanup stale worktrees.
**Note**: Does not automatically merge PRs - only reports readiness status for each PR.

> **State warning**: Branch state, remote tracking, and PR status change between
> invocations. Re-run all git/gh commands from Step 1.

## Steps

### 1. Identify Open PRs
Expand Down
3 changes: 3 additions & 0 deletions git-workflows/skills/wrap-up/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ description: "End-of-session cleanup after PR merge: refresh repo, run quick ret

# Post-Merge Wrap-Up

> **State warning**: Branch state, remote tracking, and PR status change between
> invocations. Re-run all git/gh commands from Step 1.

Run Steps 1 and 2 **in parallel** (they are independent). Step 3 starts as soon as
Step 1 completes (depends on its remote prune). Step 4 runs after all prior steps finish.
Provide a summary of actions taken.
Expand Down
4 changes: 4 additions & 0 deletions github-workflows/skills/finalize-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ metadata:
**FULLY AUTOMATIC** - Fully automates PR finalization: monitor, fix, prepare for merge. Assumes PR already exists.
No manual intervention required. For manual review-focused workflows, use `/review-pr`.

> **State warning**: Automated reviewers (CodeQL, Copilot, AI reviews) post
> asynchronously. CI may have re-run. Merge conflicts may have appeared.
> Re-fetch live PR state from Step 1.

## Critical Rules

1. **Wait for user approval to merge** - Report ready status, then pause for user merge command
Expand Down
3 changes: 3 additions & 0 deletions github-workflows/skills/resolve-pr-threads/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Orchestrates resolution of all unresolved PR review comments by grouping
related threads, processing each group sequentially inline to implement
fixes or provide explanations, then resolving threads via GitHub's GraphQL API.

> **State warning**: Thread IDs and resolution status change as reviews arrive.
> Re-fetch all open threads — cached thread lists from earlier are unreliable.

## Usage

```text
Expand Down
4 changes: 4 additions & 0 deletions github-workflows/skills/ship/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ allowed-tools: Bash(git *), Bash(gh *), Bash(pre-commit *), Bash(npm run lint*),
**Single command to commit, push, create PR(s), and auto-finalize everything.**
Handles commit, push, PR creation, and `/finalize-pr` in one pipeline. Never merges.

> **State warning**: Automated reviewers (CodeQL, Copilot, AI reviews) post
> asynchronously. CI may have re-run. Merge conflicts may have appeared.
> Re-fetch live PR state from Step 1.

## Rate Limit Awareness

This skill orchestrates many downstream API calls via `/finalize-pr`,
Expand Down
3 changes: 3 additions & 0 deletions github-workflows/skills/squash-merge-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Validates PR readiness and executes squash merge. If the PR is not ready,
errors immediately and suggests `/finalize-pr` to fix issues. Never fixes
issues — only merges.

> **State warning**: Branch state, remote tracking, and PR status change between
> invocations. Re-run all git/gh commands from Step 1.

## Critical Rules

- **Never fix issues** — only merge. If PR isn't ready, error and suggest `/finalize-pr`
Expand Down
9 changes: 9 additions & 0 deletions skill-guards/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "skill-guards",
"version": "2.3.2",
"description": "Ensures fresh execution of skills on every invocation via UserPromptSubmit hook",
"author": {"name": "JacobPEvans"},
"homepage": "https://github.com/JacobPEvans/claude-code-plugins",
"keywords": ["skills", "execution", "guards", "hooks"],
"license": "Apache-2.0"
}
38 changes: 38 additions & 0 deletions skill-guards/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# skill-guards

Ensures fresh execution of skills on every invocation via a UserPromptSubmit hook.

## Problem

When a skill like `/ship` is called a second time in a session, Claude may shortcut
by assuming previous results are still valid instead of re-running all commands against
live state.

## Solution

A UserPromptSubmit hook detects `/skill-name` patterns in user prompts and injects a
systemMessage reminding Claude to execute every step from scratch using current live state.

## Installation

```bash
claude plugins add jacobpevans-cc-plugins/skill-guards
```

## Usage

No manual invocation required. The hook activates automatically on every user prompt.

## Hook Behavior

- **Fires on**: Every user prompt submission
- **Detects**: `/lowercase-with-hyphens` skill invocation patterns
- **Excludes**: Common filesystem paths (`/usr`, `/tmp`, `/etc`, `/nix`, etc.)
- **Output**: `{"systemMessage": "FRESH EXECUTION: /skill — ..."}` or `{}`
- **Exit code**: Always 0 (never blocks prompts)

## Part of a Three-Layer System

1. **Global rule** (`skill-execution-integrity`) — establishes the mental model
2. **This hook** — tactical trigger at moment of invocation
3. **Skill preambles** — skill-specific state warnings in SKILL.md files
16 changes: 16 additions & 0 deletions skill-guards/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/skill-reinvocation-guard.sh",
"timeout": 3
}
]
}
]
}
}
21 changes: 21 additions & 0 deletions skill-guards/scripts/skill-reinvocation-guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
# UserPromptSubmit hook — inject fresh-execution reminder on skill invocation.

input=$(cat)
prompt=$(jq -r '.tool_input.prompt // .tool_input.content // .prompt // .content // empty' <<< "$input" 2>/dev/null) || { printf '{}'; exit 0; }

[[ -z "$prompt" ]] && { printf '{}'; exit 0; }

if [[ "$prompt" =~ (^|[[:space:]])/([a-z][a-z0-9-]+)([^a-z0-9-]|$) ]]; then
skill_name="${BASH_REMATCH[2]}"
case "$skill_name" in
usr|tmp|etc|var|bin|dev|opt|home|nix|proc|sys|run|lib|mnt|srv|boot|root|sbin) printf '{}'; exit 0 ;;
esac
jq -n --arg skill "$skill_name" '{
systemMessage: ("FRESH EXECUTION: /" + $skill + " — Step 1 now. New invocation, new live state. All prior outputs in this session are stale. Re-run every git, gh, and API command.")
}' || printf '{"systemMessage":"FRESH EXECUTION: /%s — Step 1 now. New invocation, new live state. All prior outputs in this session are stale. Re-run every git, gh, and API command."}' "$skill_name"
else
printf '{}'
fi

exit 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bats
# Test suite for skill-guards/scripts/skill-reinvocation-guard.sh
#
# Tests the UserPromptSubmit hook's skill detection and systemMessage injection:
# - No skill in prompt → empty JSON, exit 0
# - /ship → systemMessage with "FRESH EXECUTION" and skill name
# - /finalize-pr 42 → systemMessage with skill name
# - Regular text → empty JSON, exit 0
# - Filesystem path /usr/bin → empty JSON (false positive exclusion)
# - Empty prompt → empty JSON, exit 0
#
# Run with: bats tests/skill-guards/skill-reinvocation-guard/skill-reinvocation-guard.bats

setup() {
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../.." && pwd)"
SCRIPT="$REPO_ROOT/skill-guards/scripts/skill-reinvocation-guard.sh"

if [[ ! -f "$SCRIPT" ]]; then
echo "ERROR: Script not found at $SCRIPT" >&2
return 1
fi
}

# Run the hook with a given prompt string (uses jq for safe JSON construction)
run_hook_with_prompt() {
local prompt="$1"
run bash -c 'jq -n --arg p "$1" "{tool_input:{prompt:\$p}}" | /bin/bash "$2"' -- "$prompt" "$SCRIPT"
}

# ---------------------------------------------------------------------------
# TC1: No skill invocation → empty JSON, exit 0
# ---------------------------------------------------------------------------

@test "TC1: prompt without skill outputs empty JSON" {
run_hook_with_prompt "help me fix this bug"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}

# ---------------------------------------------------------------------------
# TC2: /ship → systemMessage with FRESH EXECUTION and skill name
# ---------------------------------------------------------------------------

@test "TC2: /ship triggers fresh execution message" {
run_hook_with_prompt "/ship"
[ "$status" -eq 0 ]
[[ "$output" =~ "systemMessage" ]]
[[ "$output" =~ "FRESH EXECUTION" ]]
[[ "$output" =~ "ship" ]]
}

# ---------------------------------------------------------------------------
# TC3: /finalize-pr 42 → systemMessage with skill name
# ---------------------------------------------------------------------------

@test "TC3: /finalize-pr with argument triggers fresh execution message" {
run_hook_with_prompt "/finalize-pr 42"
[ "$status" -eq 0 ]
[[ "$output" =~ "systemMessage" ]]
[[ "$output" =~ "FRESH EXECUTION" ]]
[[ "$output" =~ "finalize-pr" ]]
}

# ---------------------------------------------------------------------------
# TC4: /resolve-pr-threads all → systemMessage with skill name
# ---------------------------------------------------------------------------

@test "TC4: /resolve-pr-threads triggers fresh execution message" {
run_hook_with_prompt "/resolve-pr-threads all"
[ "$status" -eq 0 ]
[[ "$output" =~ "systemMessage" ]]
[[ "$output" =~ "resolve-pr-threads" ]]
}

# ---------------------------------------------------------------------------
# TC5: Regular text without slash → empty JSON
# ---------------------------------------------------------------------------

@test "TC5: text mentioning 'ship' without slash outputs empty JSON" {
run_hook_with_prompt "help me ship this feature"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}

# ---------------------------------------------------------------------------
# TC6: Filesystem path /usr/bin → empty JSON (false positive exclusion)
# ---------------------------------------------------------------------------

@test "TC6: filesystem path /usr excluded as false positive" {
run_hook_with_prompt "check /usr/bin/python"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}

# ---------------------------------------------------------------------------
# TC7: Filesystem path /tmp → empty JSON (false positive exclusion)
# ---------------------------------------------------------------------------

@test "TC7: filesystem path /tmp excluded as false positive" {
run_hook_with_prompt "read /tmp/output.log"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}

# ---------------------------------------------------------------------------
# TC8: Empty/missing prompt → empty JSON, exit 0
# ---------------------------------------------------------------------------

@test "TC8: empty input outputs empty JSON" {
run bash -c "echo '{}' | /bin/bash '$SCRIPT'"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}

# ---------------------------------------------------------------------------
# TC9: Malformed JSON input → empty JSON, exit 0 (fail-open)
# ---------------------------------------------------------------------------

@test "TC9: malformed JSON input outputs empty JSON" {
run bash -c "echo 'not json at all' | /bin/bash '$SCRIPT'"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}

# ---------------------------------------------------------------------------
# TC10: /nix path excluded as false positive
# ---------------------------------------------------------------------------

@test "TC10: filesystem path /nix excluded as false positive" {
run_hook_with_prompt "look at /nix/store/abc123"
[ "$status" -eq 0 ]
[[ "$output" == "{}" ]]
}
Loading