Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.
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
43 changes: 33 additions & 10 deletions skills/meta/writing-skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,22 @@ The entire skill creation process follows RED-GREEN-REFACTOR.

## When to Create a Skill

**Create when:**
**Create global skill when:**
- Technique wasn't intuitively obvious to you
- You'd reference this again across projects
- Pattern applies broadly (not project-specific)
- Others would benefit
- Others would benefit from it

**Don't create for:**
**Create project skill (`.claude/skills/`) when:**
- Team-specific workflows and conventions
- Project architecture patterns
- Domain-specific best practices
- Onboarding patterns for this codebase

**Don't create skill for:**
- One-off solutions
- Standard practices well-documented elsewhere
- Project-specific conventions (put in CLAUDE.md)
- Simple reminders (use CLAUDE.md instead)

## Skill Types

Expand All @@ -71,16 +77,33 @@ API docs, syntax guides, tool documentation (office docs)

## Directory Structure

**All skills are in the skills repository at `${SUPERPOWERS_SKILLS_ROOT}`:**
**Global skills** (in your branch at `${SUPERPOWERS_SKILLS_ROOT}`):

```
${SUPERPOWERS_SKILLS_ROOT}/
skills/
category/
skill-name/
SKILL.md # Main reference (required)
supporting-file.* # Only if needed
```

**Project skills** (version-controlled with project):

```
${SUPERPOWERS_SKILLS_ROOT}
skill-name/
SKILL.md # Main reference (required)
supporting-file.* # Only if needed
.claude/
skills/
category/
skill-name/
SKILL.md # Same structure as global
tool/ # Executable scripts if needed
```

**Flat namespace** - all skills in one searchable location
**Key differences:**
- Global skills: Broadly applicable, personal working branch
- Project skills: Team-shared, version-controlled
- Current dir project skills shadow global skills when paths match
- Additional context project skills (from additionalDirectories) shown with location tags

**Separate files for:**
1. **Heavy reference** (100+ lines) - API docs, comprehensive syntax
Expand Down
4 changes: 2 additions & 2 deletions skills/using-skills/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
name: Getting Started with Skills
description: Skills wiki intro - mandatory workflows, search tool, brainstorming triggers
description: Skills wiki intro - mandatory workflows, search tool, context-aware project skills
when_to_use: when starting any conversation
version: 4.0.2
version: 4.1.0
---

# Getting Started with Skills
Expand Down
205 changes: 192 additions & 13 deletions skills/using-skills/find-skills
Original file line number Diff line number Diff line change
@@ -1,13 +1,114 @@
#!/usr/bin/env bash
# find-skills - Find and list skills with when_to_use guidance
# Shows all skills by default, filters by pattern if provided
# Searches current dir + additionalDirectories from settings, then global skills

set -euo pipefail

# Determine directories
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"

# Function to get additional directories from Claude Code settings
# Outputs: "path|settings_dir" for each additionalDirectory found
get_additional_dirs() {
local git_root
git_root=$(git rev-parse --show-toplevel 2>/dev/null) || return

local current_dir
current_dir=$(pwd)

# Walk up from current dir to git root, checking for settings files
local check_dir="$current_dir"
while true; do
local settings_files=(
"$check_dir/.claude/settings.json"
"$check_dir/.claude/settings.local.json"
)

for settings_file in "${settings_files[@]}"; do
if [[ -f "$settings_file" ]]; then
# Extract paths from additionalDirectories array only
# Output format: "path|settings_dir" so we can resolve relative paths correctly
awk -v settings_dir="$check_dir" '
/"additionalDirectories"/ { in_section=1; next }
in_section && /\]/ { in_section=0; next }
in_section && /^[[:space:]]*"/ {
gsub(/^[[:space:]]*"|"[[:space:]]*,?[[:space:]]*$/, "")
print $0 "|" settings_dir
}
' "$settings_file" 2>/dev/null
fi
done

# Stop if we've reached git root
if [[ "$(cd "$check_dir" && pwd)" == "$(cd "$git_root" && pwd)" ]]; then
break
fi

# Move up one directory
check_dir=$(dirname "$check_dir")

# Safety check
if [[ "$check_dir" == "/" ]]; then
break
fi
done
}
Comment on lines +12 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify JSON parsing handles edge cases correctly.

The awk-based JSON parser should handle most simple cases of additionalDirectories arrays. However, it may not correctly handle:

  • Escaped quotes within paths (e.g., "path/with\"quote")
  • Multiline strings (unlikely in path arrays but possible)
  • Comments in JSON files (not strictly valid but sometimes present)

Run the following script to test edge cases:


🏁 Script executed:

#!/bin/bash
# Test get_additional_dirs with various JSON edge cases

# Create test directory structure
mkdir -p /tmp/test-skill-parsing/.claude
cd /tmp/test-skill-parsing

# Test 1: Normal case
cat > .claude/settings.json <<'EOF'
{
  "additionalDirectories": [
    "../project1",
    "/absolute/path"
  ]
}
EOF

# Test 2: Edge case with spaces and special characters
cat > .claude/settings.local.json <<'EOF'
{
  "additionalDirectories": [
    "../project with spaces",
    "../project-with-dashes"
  ]
}
EOF

# Extract the get_additional_dirs function and test it
# (Would need to source the actual script and run the function)

echo "Manual verification needed: Check if paths with spaces, dashes, and special characters are correctly extracted"

Length of output: 110


🏁 Script executed:

#!/bin/bash
set -e
# Prepare test under repo root so git rev-parse succeeds
repo_root=$(git rev-parse --show-toplevel)
cd "$repo_root"

# Clean up any prior test
rm -rf tmp_test
# Create nested .claude with edge-case JSON
mkdir -p tmp_test/a/b/c/.claude
cat > tmp_test/a/b/c/.claude/settings.json << 'EOF'
{
  "additionalDirectories": [
    "../proj1",
    "../proj\"quote",
    "/abs/path",
    "../withcomment" // inline comment
  ]
}
EOF

# Invoke get_additional_dirs from the script
cd tmp_test/a/b/c
source "$repo_root/skills/using-skills/find-skills"
get_additional_dirs

Length of output: 98


Replace AWK-based JSON parsing with jq
AWK fails on escaped quotes, inline comments and other JSON edge cases; a dedicated JSON parser (e.g. jq) guarantees correct extraction of additionalDirectories.

🤖 Prompt for AI Agents
In skills/using-skills/find-skills around lines 12 to 57, the current AWK-based
JSON extraction can break on escaped quotes/comments and other JSON edge cases;
replace the AWK block with a jq-based extraction: for each found settings_file
call jq to safely read .additionalDirectories[] and output each entry joined
with the settings directory as "path|settings_dir"; ensure you handle missing
.additionalDirectories gracefully (no output) and add a fallback error message
if jq is not installed so the function fails clearly.


# Collect additional directories from settings (excluding current dir)
CURRENT_DIR=$(pwd)
ADDITIONAL_DIRS=()

if git rev-parse --show-toplevel >/dev/null 2>&1; then
while IFS='|' read -r dir settings_dir; do
[[ -z "$dir" ]] && continue

# Resolve relative paths from settings_dir, absolute paths as-is
if [[ "$dir" = /* ]]; then
# Absolute path - use as is
dir_normalized=$(cd "$dir" 2>/dev/null && pwd) || continue
else
# Relative path - resolve from where settings file is
dir_normalized=$(cd "$settings_dir" && cd "$dir" 2>/dev/null && pwd) || continue
fi

current_normalized=$(cd "$CURRENT_DIR" 2>/dev/null && pwd)

# Skip if it's the current directory
if [[ "$dir_normalized" == "$current_normalized" ]]; then
continue
fi

# Deduplicate: only add if not already in array
already_added=false
for existing_dir in "${ADDITIONAL_DIRS[@]}"; do
if [[ "$existing_dir" == "$dir_normalized" ]]; then
already_added=true
break
fi
done

if ! $already_added; then
ADDITIONAL_DIRS+=("$dir_normalized")
fi
done < <(get_additional_dirs)
fi

# Project skills: current dir .claude/skills
PROJECT_SKILLS_DIR=""
if [[ -d "$CURRENT_DIR/.claude/skills" ]]; then
PROJECT_SKILLS_DIR="$CURRENT_DIR/.claude/skills"
fi

# Additional context skills: .claude/skills in each additionalDirectory
ADDITIONAL_SKILLS_DIRS=()
for dir in "${ADDITIONAL_DIRS[@]}"; do
if [[ -d "$dir/.claude/skills" ]]; then
ADDITIONAL_SKILLS_DIRS+=("$dir/.claude/skills")
fi
done

SUPERPOWERS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/superpowers"
LOG_FILE="${SUPERPOWERS_DIR}/search-log.jsonl"

Expand All @@ -28,11 +129,15 @@ EXAMPLES:

OUTPUT:
Each line shows: Use skill-path/SKILL.md when [trigger]
Skills from additionalDirectories show location: Use skill-path (dirname) when [trigger]
Paths include /SKILL.md for direct use with Read tool
Current dir project skills shadow additional directory skills when paths match

SEARCH:
Searches both skill content AND path names.
Skills location: ~/.config/superpowers/skills/
Current dir: .claude/skills/ in current working directory
Additional context: .claude/skills/ in each additionalDirectories from .claude/settings*.json
Global skills: ~/.config/superpowers/skills/
EOF
exit 0
fi
Expand All @@ -54,7 +159,8 @@ get_skill_path() {
echo "$rel_path"
}

# Collect all matching skills
# Collect all matching skills (track seen skills to handle shadowing)
seen_skills_list=""
results=()

# If pattern provided, log the search
Expand All @@ -63,23 +169,86 @@ if [[ -n "$PATTERN" ]]; then
echo "{\"timestamp\":\"$timestamp\",\"query\":\"$PATTERN\"}" >> "$LOG_FILE" 2>/dev/null || true
fi

# Search skills directory
# Search current directory project skills (no location tag)
if [[ -n "$PROJECT_SKILLS_DIR" ]]; then
while IFS= read -r file; do
[[ -z "$file" ]] && continue

# Get path relative to .claude/skills/, then prepend "skills/" for consistency
rel_path="${file#$PROJECT_SKILLS_DIR/}"
skill_path="skills/${rel_path}"

when_to_use=$(get_when_to_use "$file")
seen_skills_list="${seen_skills_list}${skill_path}"$'\n'
results+=("$skill_path||$when_to_use") # Empty location tag
done < <(
if [[ -n "$PATTERN" ]]; then
# Pattern mode: search content and paths
{
grep -E -r "$PATTERN" "$PROJECT_SKILLS_DIR/" --include="SKILL.md" -l 2>/dev/null || true
find "$PROJECT_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null | grep -E "$PATTERN" 2>/dev/null || true
} | sort -u
else
# Show all
find "$PROJECT_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null || true
fi
)
fi

# Search additional context directories (with location tags)
for ADDITIONAL_SKILLS_DIR in "${ADDITIONAL_SKILLS_DIRS[@]}"; do
# Get project directory name for location tag (parent of .claude/skills)
# /path/to/project/.claude/skills -> /path/to/project
project_dir=$(dirname "$(dirname "$ADDITIONAL_SKILLS_DIR")")
location_tag=$(basename "$project_dir")

while IFS= read -r file; do
[[ -z "$file" ]] && continue

rel_path="${file#$ADDITIONAL_SKILLS_DIR/}"
skill_path="skills/${rel_path}"

# Skip if already seen (shadowed by current dir)
if echo "$seen_skills_list" | grep -q "^${skill_path}$"; then
continue
fi
Comment on lines +212 to +214
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use fixed-string, exact-line matching for shadowing checks.

Regex matching can misidentify paths containing regex metacharacters, breaking shadowing.

Apply:

-        if echo "$seen_skills_list" | grep -q "^${skill_path}$"; then
+        if echo "$seen_skills_list" | grep -F -x -q "${skill_path}"; then
             continue
         fi
-    echo "$seen_skills_list" | grep -q "^${skill_path}$" && continue
+    echo "$seen_skills_list" | grep -F -x -q "${skill_path}" && continue

Also applies to: 238-239

🤖 Prompt for AI Agents
In skills/using-skills/find-skills around lines 212-214 (and also apply the same
change at 238-239): the current grep uses regex matching which misinterprets
skill paths containing regex metacharacters; change the grep invocation to use
fixed-string (-F) and exact-line (-x) matching (e.g., grep -Fx) when checking if
a skill_path is in seen_skills_list so it only matches the full literal line;
keep the surrounding logic (if ... then continue fi) unchanged.


when_to_use=$(get_when_to_use "$file")
seen_skills_list="${seen_skills_list}${skill_path}"$'\n'
results+=("$skill_path|$location_tag|$when_to_use")
done < <(
if [[ -n "$PATTERN" ]]; then
{
grep -E -r "$PATTERN" "$ADDITIONAL_SKILLS_DIR/" --include="SKILL.md" -l 2>/dev/null || true
find "$ADDITIONAL_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null | grep -E "$PATTERN" 2>/dev/null || true
} | sort -u
else
find "$ADDITIONAL_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null || true
fi
)
done

# Search global skills directory (skip if shadowed by project skills)
while IFS= read -r file; do
[[ -z "$file" ]] && continue

skill_path=$(get_skill_path "$file" "$SKILLS_DIR")

# Skip if shadowed by project skill
echo "$seen_skills_list" | grep -q "^${skill_path}$" && continue

when_to_use=$(get_when_to_use "$file")
results+=("$skill_path|$when_to_use")
results+=("$skill_path||$when_to_use") # Empty location tag
done < <(
if [[ -n "$PATTERN" ]]; then
# Pattern mode: search content and paths
# Pattern mode: search content and paths (exclude .claude directories)
{
grep -E -r -- "$PATTERN" "$SKILLS_DIR/" --include="SKILL.md" -l 2>/dev/null || true
find "$SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null | grep -E -- "$PATTERN" 2>/dev/null || true
grep -E -r -- "$PATTERN" "$SKILLS_DIR/" --include="SKILL.md" --exclude-dir=".claude" -l 2>/dev/null || true
find "$SKILLS_DIR/" -name "SKILL.md" -type f -not -path "*/.claude/*" 2>/dev/null | grep -E -- "$PATTERN" 2>/dev/null || true
} | sort -u
else
# Show all
find "$SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null || true
# Show all (exclude .claude directories)
find "$SKILLS_DIR/" -name "SKILL.md" -type f -not -path "*/.claude/*" 2>/dev/null || true
fi
)

Expand All @@ -96,11 +265,21 @@ if [[ ${#results[@]} -eq 0 ]]; then
fi

# Sort and display results
printf "%s\n" "${results[@]}" | sort | while IFS='|' read -r skill_path when_to_use; do
if [[ -n "$when_to_use" ]]; then
echo "Use $skill_path $when_to_use"
printf "%s\n" "${results[@]}" | sort | while IFS='|' read -r skill_path location_tag when_to_use; do
if [[ -n "$location_tag" ]]; then
# Additional directory skill - show with location tag
if [[ -n "$when_to_use" ]]; then
echo "Use $skill_path ($location_tag) $when_to_use"
else
echo "$skill_path ($location_tag)"
fi
else
echo "$skill_path"
# Current dir or global skill - no location tag
if [[ -n "$when_to_use" ]]; then
echo "Use $skill_path $when_to_use"
else
echo "$skill_path"
fi
fi
done

Expand Down