Claude Skill

auto-skill

Evaluate current session for skill-worthy workflows and create reusable skills. Triggers on: auto-skill, create skill from session, save workflow, capture this as a skill.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_auto-skill-3dfaf0b.zip · 10 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/auto-skill
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Auto-Skill

Evaluate the current session and create a reusable skill from complex workflows. Enforces the Agent Skills specification and quality gates.

When This Triggers

  • User runs /auto-skill
  • Stop hook suggests it after a complex session (8+ mutating ops across 4+ tool types)
  • User says "save this as a skill", "capture this workflow", etc.

Command Router

Parse arguments after auto-skill (or /auto-skill):

User says Action
auto-skill (no args) Run the full evaluation procedure below
auto-skill off Disable globally: touch ~/.claude/auto-skill.disable and confirm
auto-skill on Enable globally: rm -f ~/.claude/auto-skill.disable and confirm
auto-skill off --project Disable for this project: mkdir -p .claude && touch .claude/auto-skill.disable
auto-skill on --project Enable for this project: rm -f .claude/auto-skill.disable
auto-skill status Show current state (see Status section below)
auto-skill pending Show all entries in ~/.claude/auto-skill/pending.log (past suggestions the user may have missed)
auto-skill clear Truncate ~/.claude/auto-skill/pending.log after confirming with user

Status

When the user runs auto-skill status, check and report:

# Global toggle
[ -f "$HOME/.claude/auto-skill.disable" ] && echo "Global: OFF" || echo "Global: ON"

# Project toggle
[ -f ".claude/auto-skill.disable" ] && echo "Project: OFF" || echo "Project: ON"

# Hook scripts installed?
[ -x "$HOME/.claude/auto-skill/track-tools.sh" ] && echo "Hooks: installed" || echo "Hooks: not installed"

# Active session tracking?
ls /tmp/claude_autoskill_* 2>/dev/null | head -1 && echo "Tracking: active" || echo "Tracking: idle"

Report results in a brief table.

Procedure

Step 1: Evaluate the Session

Review the conversation history in the current session. Ask yourself:

  1. Was this a multi-step workflow? (3+ distinct actions, not just read/search)
  2. Is it reusable? Would someone do this again - in this project or another?
  3. Is it novel? Does an existing skill already cover this? Check with:
    ls ~/.claude/skills/ 2>/dev/null; ls .claude/skills/ 2>/dev/null
    
  4. Is it teachable? Can it be described as a clear procedure with steps?

If ANY answer is no, tell the user: "This session doesn't look like a good skill candidate" and explain which criterion failed. Stop here.

Step 2: Duplicate Detection

Before creating, check for overlapping skills:

# List existing skill names and descriptions
for f in ~/.claude/skills/*/SKILL.md .claude/skills/*/SKILL.md 2>/dev/null; do
  [ -f "$f" ] || continue
  name=$(head -10 "$f" | grep '^name:' | sed 's/name: *//')
  desc=$(head -10 "$f" | grep '^description:' | sed 's/description: *//' | tr -d '"')
  echo "$name: $desc"
done

Block if:

  • Exact name match exists
  • 60%+ word overlap in proposed name vs existing name
  • 50%+ word overlap in proposed description vs existing description

If overlap detected, suggest extending the existing skill instead.

Step 3: Draft the Skill

Propose a skill to the user with:

Field Value
Name kebab-case, descriptive, matches what it does
Description 1-2 sentences with trigger keywords
Procedure Numbered steps extracted from the session workflow
Tools needed Which tools the skill requires

Ask the user to confirm or adjust before creating.

Step 4: Quality Gates

Before writing, validate:

Gate Requirement Why
Name format ^[a-z][a-z0-9-]*$, 1-64 chars Agent Skills spec
Description Non-empty, 1-1024 chars, includes trigger phrases Spec + discovery
Procedure Must contain numbered steps, ## Procedure/## Steps, or checkboxes Ensures actionable content
Min content 200+ characters in body (after frontmatter) Rejects trivial stubs
License license: MIT claude-mods convention
Metadata metadata.author: claude-mods claude-mods convention
No non-standard top-level keys Only name, description, license, compatibility, allowed-tools, metadata Agent Skills spec

If any gate fails, explain which one and help the user fix it.

Step 5: Create the Skill

Write the skill to the project's skill directory:

.claude/skills/<skill-name>/
  SKILL.md
  scripts/.gitkeep
  references/.gitkeep
  assets/.gitkeep

SKILL.md frontmatter template (Agent Skills spec compliant):

---
name: <kebab-case-name>
description: "<what it does>. Triggers on: <keyword1>, <keyword2>, <keyword3>."
license: MIT
allowed-tools: "<space-delimited tool list>"
metadata:
  author: claude-mods
---

Body structure:

# <Skill Title>

<1-2 sentence overview>

## When to Use

- <trigger condition 1>
- <trigger condition 2>

## Procedure

1. <Step one>
2. <Step two>
3. <Step three>
...

## Notes

<Edge cases, caveats, or tips>

Step 6: Verify

After creating, verify the skill:

  1. Check the file was written correctly:
    head -20 .claude/skills/<name>/SKILL.md
    
  2. Validate frontmatter has only spec-compliant top-level keys
  3. Confirm the procedure section exists and has steps
  4. Tell the user the skill is ready and how to invoke it

Pending Log

Because systemMessage output from the Stop hook is delivered to Claude (not directly to the user), suggestions often die silently when the user's next prompt doesn't invite them to be mentioned. To solve this, the hook also appends a line to ~/.claude/auto-skill/pending.log each time it fires:

2026-04-24T19:28:03+10:00|9dc8576c|/x/forge/axiom|12|5|28|Write(4) Edit(3) Bash(3)

Fields (pipe-delimited):

# Field Example
1 ISO8601 timestamp 2026-04-24T19:28:03+10:00
2 Short session ID 9dc8576c
3 CWD when suggestion fired /x/forge/axiom
4 Mutating op count 12
5 Unique tool type count 5
6 Total tool calls 28
7 Top-6 tool histogram Write(4) Edit(3) Bash(3)

/sync reads this log at session start and surfaces any entries from the last 72 hours under a "Skill Suggestions" section — the one place the user will reliably see them.

Viewing and clearing

  • auto-skill pending — cat ~/.claude/auto-skill/pending.log (or show "no pending suggestions" if absent/empty)
  • auto-skill clear — truncate after confirming with the user

Per-Project Disable

touch .claude/auto-skill.disable    # Disable Stop hook suggestions
rm .claude/auto-skill.disable       # Re-enable

The skill itself can always be invoked manually regardless of this setting.

Hook Setup

Auto-skill uses two hooks for automatic suggestions. These are installed globally:

~/.claude/auto-skill/
  track-tools.sh     # PostToolUse: counts tool calls per session
  evaluate.sh        # Stop: suggests skill creation if complex enough

Both hooks fail silently - they will never produce error output or block Claude.

Hook Configuration

Add to ~/.claude/settings.json (merge with existing hooks):

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "bash \"$HOME/.claude/auto-skill/track-tools.sh\"",
        "timeout": 2
      }]
    }],
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "bash \"$HOME/.claude/auto-skill/evaluate.sh\"",
        "timeout": 5
      }]
    }]
  }
}

Suggestion Gates

The Stop hook only suggests skill creation when ALL of these pass:

Gate Threshold Rationale
Mutating ops 8+ High bar reduces noise from routine edits
Tool diversity 4+ distinct types Write+Edit+Bash+Agent = workflow; Write*20 = repetitive
No non-harness skill loaded Skill tool absent OR only harness skills If following a domain skill, work isn't novel. Harness skills (sync, save, introspect, auto-skill, setperms, tool-discovery) are whitelisted — they're bootstrap/meta, not recipes.
Per-session Once per session Never nags on resume/continue
Not disabled No .disable file Global or per-project toggle

Read-only tools (Read, Glob, Grep, LS, Task*) are excluded from counts.

Design Decisions

  • Stop hook, not in-loop: Claude Code doesn't expose the agent loop. The Stop hook fires while context is still in memory, which is the best we can get.
  • systemMessage output: The Stop hook outputs JSON that Claude Code displays to the user. Non-blocking, dismissible.
  • Diversity over volume: Tool type count matters more than raw call count. A 20-file rename isn't a skill; a workflow using Write+Edit+Bash+Agent probably is.
  • Per-session cooldown: Uses a temp file keyed by session ID. No harsh time-based cooldown - each new session gets a fresh chance.
  • 500-line cap on tracking: Prevents runaway sessions from filling /tmp.
  • Silent failures: Both hooks wrap everything in 2>/dev/null and always exit 0.
Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
  • references
    • .gitkeep 0 B · in bundle
  • scripts
    • .gitkeep 0 B · in bundle
    • evaluate.sh 4.9 KB
      #!/bin/bash
      # evaluate.sh - Stop hook: evaluate session for skill-worthy workflows
      #
      # Suggests skill creation only when a session shows genuine workflow complexity:
      #   - 8+ mutating tool calls (high threshold, reduces noise)
      #   - 4+ distinct mutating tool types (diversity = workflow, not repetitive edits)
      #   - No non-harness skill was loaded (novel work, not following a recipe).
      #     Harness skills (sync, save, introspect, auto-skill, setperms, tool-discovery)
      #     are whitelisted — they're meta/bootstrap, not domain-specific, so loading
      #     them shouldn't disqualify an otherwise novel session.
      #   - Per-session cooldown file prevents re-fire on resume
      #
      # Output channels (when a suggestion fires):
      #   1. systemMessage JSON on stdout - visible to Claude on next turn
      #   2. Appended line to ~/.claude/auto-skill/pending.log - visible to user
      #      at next /sync (since Claude's systemMessage often dies silently if
      #      the user's next prompt doesn't invite it to be mentioned).
      #
      # Toggle: touch ~/.claude/auto-skill.disable   (global off)
      #         touch .claude/auto-skill.disable      (project off)
      #         rm either file to re-enable
      #
      # CRITICAL: This hook must NEVER fail visibly. All errors suppressed.
      
      {
        INPUT=$(cat)
        SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
      
        [ -z "$SESSION_ID" ] && exit 0
      
        SHORT_ID="${SESSION_ID:0:8}"
        TRACK_FILE="/tmp/claude_autoskill_${SHORT_ID}"
      
        # No tracking file = no tool calls recorded
        [ -f "$TRACK_FILE" ] || exit 0
      
        # --- Toggle: global or project disable ---
        if [ -f "$HOME/.claude/auto-skill.disable" ] || [ -f ".claude/auto-skill.disable" ]; then
          rm -f "$TRACK_FILE"
          exit 0
        fi
      
        # --- Per-session cooldown: only suggest once per session ---
        SUGGESTED_FILE="/tmp/claude_autoskill_suggested_${SHORT_ID}"
        if [ -f "$SUGGESTED_FILE" ]; then
          rm -f "$TRACK_FILE"
          exit 0
        fi
      
        # --- Classify tools ---
        READ_ONLY_LIST=" Read Glob Grep LS NotebookRead TaskList TaskGet TaskCreate TaskUpdate TaskOutput TaskStop "
        # Harness skills: loading these should NOT disqualify a session
        HARNESS_SKILLS=" sync save introspect auto-skill setperms tool-discovery "
        SKILL_LOADED=false
        TOTAL=0
        WRITES=0
        UNIQUE_TYPES=""
      
        while IFS= read -r tool; do
          [ -z "$tool" ] && continue
          TOTAL=$((TOTAL + 1))
      
          # Handle Skill tool (tagged as "Skill:<name>" by track-tools.sh, or bare
          # "Skill" from pre-whitelist versions)
          case "$tool" in
            Skill:*)
              skill_name="${tool#Skill:}"
              # Is it a harness skill? If so, ignore entirely.
              case "$HARNESS_SKILLS" in
                *" ${skill_name} "*) continue ;;
                *) SKILL_LOADED=true; continue ;;
              esac
              ;;
            Skill)
              # Legacy format (pre-whitelist): conservatively disqualify
              SKILL_LOADED=true
              continue
              ;;
          esac
      
          # Check if read-only (space-padded list for exact word match)
          case "$READ_ONLY_LIST" in
            *" ${tool} "*) continue ;;
          esac
      
          WRITES=$((WRITES + 1))
      
          # Track unique mutating tool types
          case " $UNIQUE_TYPES " in
            *" ${tool} "*) ;;  # already seen
            *) UNIQUE_TYPES="${UNIQUE_TYPES} ${tool}" ;;
          esac
        done < "$TRACK_FILE"
      
        # Count unique types (count words in UNIQUE_TYPES)
        UNIQUE_COUNT=0
        for _ in $UNIQUE_TYPES; do
          UNIQUE_COUNT=$((UNIQUE_COUNT + 1))
        done
      
        # Build tool summary before cleanup
        TOOL_SUMMARY=$(sort "$TRACK_FILE" | uniq -c | sort -rn | head -6 | awk '{printf "%s(%d) ", $2, $1}')
      
        # Clean up tracking file
        rm -f "$TRACK_FILE"
      
        # --- Gate 1: Non-harness skill was loaded = following a recipe, not novel ---
        [ "$SKILL_LOADED" = true ] && exit 0
      
        # --- Gate 2: Minimum 8 mutating operations ---
        [ "$WRITES" -lt 8 ] && exit 0
      
        # --- Gate 3: Minimum 4 distinct mutating tool types ---
        [ "$UNIQUE_COUNT" -lt 4 ] && exit 0
      
        # --- All gates passed: suggest ---
      
        # Mark this session as suggested (prevents repeat on resume)
        touch "$SUGGESTED_FILE" 2>/dev/null
      
        # Append to persistent log so the human can see suggestions at next /sync.
        # systemMessage goes to Claude; this log goes to the user.
        # Format: ISO8601 | session_id | cwd | writes | unique | total | summary
        LOG_DIR="$HOME/.claude/auto-skill"
        LOG_FILE="$LOG_DIR/pending.log"
        mkdir -p "$LOG_DIR" 2>/dev/null
        TS=$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')
        CWD=$(pwd 2>/dev/null || echo "unknown")
        CLEAN_SUMMARY=$(printf '%s' "$TOOL_SUMMARY" | tr '|' '/' | tr -s ' ')
        printf '%s|%s|%s|%d|%d|%d|%s\n' \
          "$TS" "$SHORT_ID" "$CWD" "$WRITES" "$UNIQUE_COUNT" "$TOTAL" "$CLEAN_SUMMARY" \
          >> "$LOG_FILE" 2>/dev/null
      
        MSG="Skill-worthy session: ${WRITES} mutating ops across ${UNIQUE_COUNT} tool types (${TOTAL} total): ${TOOL_SUMMARY}- run /auto-skill to capture this workflow."
      
        ESCAPED=$(printf '%s' "$MSG" | sed 's/"/\\"/g' | tr '\n' ' ')
        printf '{"systemMessage":"%s"}\n' "$ESCAPED"
      
      } 2>/dev/null
      
      exit 0
      
    • track-tools.sh 1.4 KB
      #!/bin/bash
      # track-tools.sh - PostToolUse hook: lightweight tool call counter
      # Appends tool name to a session-specific temp file.
      # Designed to be fast (<5ms) - no SQLite, no network, just a file append.
      #
      # Special case: when the Skill tool is invoked, we record `Skill:<name>`
      # instead of bare `Skill`. evaluate.sh uses the name to whitelist harness
      # skills (sync, save, etc) from Gate 1 disqualification.
      #
      # CRITICAL: This hook must NEVER fail visibly. All errors suppressed.
      
      {
        INPUT=$(cat)
        TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
        SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
      
        [ -z "$TOOL_NAME" ] && exit 0
        [ -z "$SESSION_ID" ] && exit 0
      
        SHORT_ID="${SESSION_ID:0:8}"
        TRACK_FILE="/tmp/claude_autoskill_${SHORT_ID}"
      
        # Tag Skill tool calls with the skill name so Gate 1 can whitelist
        if [ "$TOOL_NAME" = "Skill" ]; then
          SKILL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_input.skill // "unknown"' 2>/dev/null)
          # Sanitise: keep parsing simple by normalising separators
          SKILL_NAME=$(printf '%s' "$SKILL_NAME" | tr ': ' '_')
          TOOL_NAME="Skill:${SKILL_NAME}"
        fi
      
        # Append tool name (one per line). Cap at 500 lines to prevent runaway.
        if [ ! -f "$TRACK_FILE" ] || [ "$(wc -l < "$TRACK_FILE" 2>/dev/null)" -lt 500 ]; then
          echo "$TOOL_NAME" >> "$TRACK_FILE"
        fi
      } 2>/dev/null
      
      exit 0
      
  • tests
    • run.sh 9.7 KB
      #!/usr/bin/env bash
      # Self-test for auto-skill — sandboxed, offline, never touches real state.
      #
      # evaluate.sh ships real side effects: it reads a per-session tracking file
      # (/tmp/claude_autoskill_<8>), classifies the session, then `rm -f`s the tracking
      # file and appends to ~/.claude/auto-skill/pending.log. track-tools.sh is the
      # PostToolUse writer that populates that tracking file. Testing them must NEVER
      # touch the real ~/.claude/auto-skill/ or the repo's skills/ tree.
      #
      # Isolation strategy:
      #   - HOME is redirected to a throwaway sandbox for EVERY evaluate.sh call, so
      #     pending.log + the disable-toggle check land in the sandbox, not real ~.
      #   - Each scenario gets a UNIQUE 8-char session-id prefix (the first 8 chars of
      #     the session id select the tracking file). The prefix starts with 'g', which
      #     never appears in a real (hex-UUID) Claude session id, so our tracking files
      #     can never collide with a real running session. gen_sid is called WITHOUT
      #     command substitution so its counter survives (a `$(...)` subshell would
      #     discard the increment).
      #   - The trap removes our 'g'-prefixed tracking + suggested files (and only
      #     those) on exit.
      #
      # Coverage:
      #   - track-tools.sh records bare tool names and tags Skill:<name> (sanitised).
      #   - evaluate.sh classification: fires on qualifying sessions and when only a
      #     harness skill was loaded; stays silent on too-few-writes / too-few-types /
      #     non-harness-skill / cooldown.
      #   - evaluate.sh reset/safety: the reset path deletes ONLY the intended
      #     tracking file (siblings survive); the happy path cleans the tracking file
      #     and writes pending.log into the SANDBOX, not real HOME.
      #
      # Usage:   bash tests/run.sh
      # Exit:    0 all pass, 1 one or more failures
      
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      EVAL="$SKILL/scripts/evaluate.sh"
      TRACK_TOOL="$SKILL/scripts/track-tools.sh"
      
      SB="$(mktemp -d)"; mkdir -p "$SB/home"
      cleanup() {
          rm -rf "$SB" 2>/dev/null
          # 'g' prefix is never a real (hex-UUID) session id → only our files match.
          rm -f /tmp/claude_autoskill_g* /tmp/claude_autoskill_suggested_g* 2>/dev/null
      }
      trap cleanup EXIT
      
      # Unique per-run, per-scenario 8-char session-id prefixes. SID_BASE from the PID
      # keeps parallel runs apart; SEQ increments per call (in the parent shell, NOT a
      # subshell). The leading 'g' avoids any real Claude session id (hex UUID).
      SID_BASE=$(( $$ % 10000 ))
      SEQ=0
      GEN_SID=""
      gen_sid() { SEQ=$((SEQ+1)); GEN_SID=$(printf 'g%04d%03d' "$SID_BASE" "$SEQ"); }
      track_file()    { printf '/tmp/claude_autoskill_%s'            "$1"; }
      suggested_file(){ printf '/tmp/claude_autoskill_suggested_%s'  "$1"; }
      
      # Write tool names (one per line) into a tracking file.
      write_track() { local f="$1"; shift; : > "$f"; for t in "$@"; do printf '%s\n' "$t" >> "$f"; done; }
      
      # Run evaluate.sh with a session id: HOME=sandbox, CWD=sandbox (no project
      # disable file). Captures stdout only (evaluate always exits 0, stderr muted).
      run_eval() { ( cd "$SB" && printf '{"session_id":"%sxxxxxxxxxxxx"}' "$1" \
                          | HOME="$SB/home" bash "$EVAL" ); }
      # Run track-tools.sh with a tool/session payload, HOME=sandbox.
      run_track() { # $1=session_id  $2=tool_name  $3=skill_name(or '')
          local payload
          if [[ -n "$3" ]]; then
              payload=$(printf '{"tool_name":"%s","session_id":"%sxxxxxxxxxxxx","tool_input":{"skill":"%s"}}' "$2" "$1" "$3")
          else
              payload=$(printf '{"tool_name":"%s","session_id":"%sxxxxxxxxxxxx"}' "$2" "$1")
          fi
          ( cd "$SB" && printf '%s' "$payload" | HOME="$SB/home" bash "$TRACK_TOOL" )
      }
      
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; }
      expect_has()  { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
      
      echo "=== auto-skill self-test (sandboxed) ==="
      
      # ── syntax ───────────────────────────────────────────────────────────────────
      echo "-- syntax --"
      bash -n "$EVAL"      2>/dev/null && ok "bash -n evaluate.sh"      || no "bash -n evaluate.sh"
      bash -n "$TRACK_TOOL" 2>/dev/null && ok "bash -n track-tools.sh"  || no "bash -n track-tools.sh"
      
      # ── track-tools.sh: records bare names + tags Skill:<name> ───────────────────
      echo "-- track-tools.sh writer --"
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      run_track "$SID" "Edit" ""
      run_track "$SID" "Bash" ""
      run_track "$SID" "Skill" "sync"
      run_track "$SID" "Skill" "deep-research"
      run_track "$SID" "Skill" "my:weird skill"
      contents="$(cat "$TF" 2>/dev/null)"
      expect_has "records bare tool name"  $'Edit'                "$contents"
      expect_has "records bare Bash"       $'Bash'                "$contents"
      expect_has "tags harness skill"      "Skill:sync"           "$contents"
      expect_has "tags non-harness skill"  "Skill:deep-research"  "$contents"
      expect_has "sanitises separators"    "Skill:my_weird_skill" "$contents"
      # written to the per-session file derived from the first 8 chars of session_id
      [[ -f "$TF" ]] && ok "track file path derives from session-id prefix" \
                      || no "track file path derives from session-id prefix"
      
      # ── evaluate.sh classification ───────────────────────────────────────────────
      echo "-- evaluate.sh classification (fires) --"
      # Qualifying session: 9 mutating ops, 5 distinct types, no skill -> fires.
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
      out="$(run_eval "$SID")"
      expect_has "qualify -> systemMessage" "systemMessage"   "$out"
      expect_has "qualify reports mutating count" "9 mutating ops" "$out"
      expect_has "qualify reports type count"     "5 tool types"   "$out"
      expect_has "qualify reports total"          "(9 total)"      "$out"
      
      # Harness skill loaded (sync) must NOT disqualify -> still fires.
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Skill:sync Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
      out="$(run_eval "$SID")"
      expect_has "harness skill does not disqualify" "systemMessage" "$out"
      expect_has "harness-fire counts skill in total" "(10 total)"   "$out"
      
      echo "-- evaluate.sh classification (silent) --"
      # Too few mutating ops (7 < 8).
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Edit Edit Edit Write Write Write Bash
      out="$(run_eval "$SID")"
      [[ -z "$out" ]] && ok "too few writes (7) -> silent" || no "too few writes should be silent: [$out]"
      
      # Enough writes but too few distinct types (3 < 4).
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Edit Edit Edit Edit Write Write Bash Bash
      out="$(run_eval "$SID")"
      [[ -z "$out" ]] && ok "too few distinct types (3) -> silent" || no "too few types should be silent: [$out]"
      
      # Non-harness skill loaded -> Gate 1 disqualifies.
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Skill:deep-research Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
      out="$(run_eval "$SID")"
      [[ -z "$out" ]] && ok "non-harness skill -> silent" || no "non-harness skill should be silent: [$out]"
      
      # ── evaluate.sh reset / safety ───────────────────────────────────────────────
      echo "-- evaluate.sh reset/safety --"
      # Happy path cleans the tracking file and writes pending.log into the SANDBOX.
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
      run_eval "$SID" >/dev/null
      [[ ! -f "$TF" ]]                       && ok "happy path removes tracking file" || no "happy path should remove tracking file"
      [[ -f "$SB/home/.claude/auto-skill/pending.log" ]] \
          && ok "pending.log lands in sandbox HOME (not real ~)" \
          || no "pending.log should land in sandbox HOME"
      
      # Reset path (cooldown) deletes ONLY the intended tracking file: the per-session
      # SUGGESTED marker and an unrelated sibling tracking file must survive.
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID"); SG=$(suggested_file "$SID")
      gen_sid; OTHER="$GEN_SID"; DEC=$(track_file "$OTHER")
      write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
      printf 'cooldown-marker\n' > "$SG"
      printf 'decoy\n'            > "$DEC"
      out="$(run_eval "$SID")"
      [[ -z "$out" ]]  && ok "cooldown -> silent"             || no "cooldown should be silent"
      [[ ! -f "$TF" ]] && ok "cooldown removes tracking file" || no "cooldown should remove tracking file"
      [[  -f "$SG" ]]  && ok "cooldown keeps SUGGESTED marker" || no "cooldown must not delete SUGGESTED marker"
      [[  -f "$DEC" ]] && ok "cooldown keeps sibling file"     || no "cooldown must not delete sibling tracking file"
      
      # Reset path (global disable): removes tracking file, no output, no pending.log.
      gen_sid; SID="$GEN_SID"; TF=$(track_file "$SID")
      write_track "$TF" Edit Edit Edit Write Write Bash Bash NotebookEdit MultiEdit
      mkdir -p "$SB/home/.claude"; touch "$SB/home/.claude/auto-skill.disable"
      LOG_BEFORE="$(wc -l < "$SB/home/.claude/auto-skill/pending.log" 2>/dev/null || echo 0)"
      out="$(run_eval "$SID")"
      [[ -z "$out" ]]  && ok "disabled -> silent"             || no "disabled should be silent"
      [[ ! -f "$TF" ]] && ok "disabled removes tracking file" || no "disabled should remove tracking file"
      LOG_AFTER="$(wc -l < "$SB/home/.claude/auto-skill/pending.log" 2>/dev/null || echo 0)"
      [[ "$LOG_AFTER" == "$LOG_BEFORE" ]] && ok "disabled writes no pending.log line" \
                                          || no "disabled must not append pending.log"
      rm -f "$SB/home/.claude/auto-skill.disable"
      
      echo ""
      echo "=== $PASS passed, $FAIL failed ==="
      [[ "$FAIL" -eq 0 ]] || exit 1
      exit 0
      
  • SKILL.md 9.4 KB
    ---
    name: auto-skill
    description: "Evaluate current session for skill-worthy workflows and create reusable skills. Triggers on: auto-skill, create skill from session, save workflow, capture this as a skill."
    license: MIT
    allowed-tools: "Read Glob Grep Bash Write Edit"
    metadata:
      author: claude-mods
      depends-on: "skill-creator"
      related-skills: "skill-creator, introspect"
    ---
    
    # Auto-Skill
    
    Evaluate the current session and create a reusable skill from complex workflows. Enforces the [Agent Skills specification](https://agentskills.io/specification) and quality gates.
    
    ## When This Triggers
    
    - User runs `/auto-skill`
    - Stop hook suggests it after a complex session (8+ mutating ops across 4+ tool types)
    - User says "save this as a skill", "capture this workflow", etc.
    
    ## Command Router
    
    Parse arguments after `auto-skill` (or `/auto-skill`):
    
    | User says | Action |
    |-----------|--------|
    | `auto-skill` (no args) | Run the full evaluation procedure below |
    | `auto-skill off` | Disable globally: `touch ~/.claude/auto-skill.disable` and confirm |
    | `auto-skill on` | Enable globally: `rm -f ~/.claude/auto-skill.disable` and confirm |
    | `auto-skill off --project` | Disable for this project: `mkdir -p .claude && touch .claude/auto-skill.disable` |
    | `auto-skill on --project` | Enable for this project: `rm -f .claude/auto-skill.disable` |
    | `auto-skill status` | Show current state (see Status section below) |
    | `auto-skill pending` | Show all entries in `~/.claude/auto-skill/pending.log` (past suggestions the user may have missed) |
    | `auto-skill clear` | Truncate `~/.claude/auto-skill/pending.log` after confirming with user |
    
    ### Status
    
    When the user runs `auto-skill status`, check and report:
    
    ```bash
    # Global toggle
    [ -f "$HOME/.claude/auto-skill.disable" ] && echo "Global: OFF" || echo "Global: ON"
    
    # Project toggle
    [ -f ".claude/auto-skill.disable" ] && echo "Project: OFF" || echo "Project: ON"
    
    # Hook scripts installed?
    [ -x "$HOME/.claude/auto-skill/track-tools.sh" ] && echo "Hooks: installed" || echo "Hooks: not installed"
    
    # Active session tracking?
    ls /tmp/claude_autoskill_* 2>/dev/null | head -1 && echo "Tracking: active" || echo "Tracking: idle"
    ```
    
    Report results in a brief table.
    
    ## Procedure
    
    ### Step 1: Evaluate the Session
    
    Review the conversation history in the current session. Ask yourself:
    
    1. **Was this a multi-step workflow?** (3+ distinct actions, not just read/search)
    2. **Is it reusable?** Would someone do this again - in this project or another?
    3. **Is it novel?** Does an existing skill already cover this? Check with:
       ```bash
       ls ~/.claude/skills/ 2>/dev/null; ls .claude/skills/ 2>/dev/null
       ```
    4. **Is it teachable?** Can it be described as a clear procedure with steps?
    
    If ANY answer is no, tell the user: "This session doesn't look like a good skill candidate" and explain which criterion failed. **Stop here.**
    
    ### Step 2: Duplicate Detection
    
    Before creating, check for overlapping skills:
    
    ```bash
    # List existing skill names and descriptions
    for f in ~/.claude/skills/*/SKILL.md .claude/skills/*/SKILL.md 2>/dev/null; do
      [ -f "$f" ] || continue
      name=$(head -10 "$f" | grep '^name:' | sed 's/name: *//')
      desc=$(head -10 "$f" | grep '^description:' | sed 's/description: *//' | tr -d '"')
      echo "$name: $desc"
    done
    ```
    
    **Block if:**
    - Exact name match exists
    - 60%+ word overlap in proposed name vs existing name
    - 50%+ word overlap in proposed description vs existing description
    
    If overlap detected, suggest extending the existing skill instead.
    
    ### Step 3: Draft the Skill
    
    Propose a skill to the user with:
    
    | Field | Value |
    |-------|-------|
    | **Name** | kebab-case, descriptive, matches what it does |
    | **Description** | 1-2 sentences with trigger keywords |
    | **Procedure** | Numbered steps extracted from the session workflow |
    | **Tools needed** | Which tools the skill requires |
    
    Ask the user to confirm or adjust before creating.
    
    ### Step 4: Quality Gates
    
    Before writing, validate:
    
    | Gate | Requirement | Why |
    |------|-------------|-----|
    | **Name format** | `^[a-z][a-z0-9-]*$`, 1-64 chars | Agent Skills spec |
    | **Description** | Non-empty, 1-1024 chars, includes trigger phrases | Spec + discovery |
    | **Procedure** | Must contain numbered steps, `## Procedure`/`## Steps`, or checkboxes | Ensures actionable content |
    | **Min content** | 200+ characters in body (after frontmatter) | Rejects trivial stubs |
    | **License** | `license: MIT` | claude-mods convention |
    | **Metadata** | `metadata.author: claude-mods` | claude-mods convention |
    | **No non-standard top-level keys** | Only `name`, `description`, `license`, `compatibility`, `allowed-tools`, `metadata` | Agent Skills spec |
    
    If any gate fails, explain which one and help the user fix it.
    
    ### Step 5: Create the Skill
    
    Write the skill to the project's skill directory:
    
    ```
    .claude/skills/<skill-name>/
      SKILL.md
      scripts/.gitkeep
      references/.gitkeep
      assets/.gitkeep
    ```
    
    **SKILL.md frontmatter template** (Agent Skills spec compliant):
    
    ```yaml
    ---
    name: <kebab-case-name>
    description: "<what it does>. Triggers on: <keyword1>, <keyword2>, <keyword3>."
    license: MIT
    allowed-tools: "<space-delimited tool list>"
    metadata:
      author: claude-mods
    ---
    ```
    
    **Body structure:**
    
    ```markdown
    # <Skill Title>
    
    <1-2 sentence overview>
    
    ## When to Use
    
    - <trigger condition 1>
    - <trigger condition 2>
    
    ## Procedure
    
    1. <Step one>
    2. <Step two>
    3. <Step three>
    ...
    
    ## Notes
    
    <Edge cases, caveats, or tips>
    ```
    
    ### Step 6: Verify
    
    After creating, verify the skill:
    
    1. Check the file was written correctly:
       ```bash
       head -20 .claude/skills/<name>/SKILL.md
       ```
    2. Validate frontmatter has only spec-compliant top-level keys
    3. Confirm the procedure section exists and has steps
    4. Tell the user the skill is ready and how to invoke it
    
    ## Pending Log
    
    Because `systemMessage` output from the Stop hook is delivered to Claude (not
    directly to the user), suggestions often die silently when the user's next
    prompt doesn't invite them to be mentioned. To solve this, the hook also
    appends a line to `~/.claude/auto-skill/pending.log` each time it fires:
    
    ```
    2026-04-24T19:28:03+10:00|9dc8576c|/x/forge/axiom|12|5|28|Write(4) Edit(3) Bash(3)
    ```
    
    Fields (pipe-delimited):
    
    | # | Field | Example |
    |---|-------|---------|
    | 1 | ISO8601 timestamp | `2026-04-24T19:28:03+10:00` |
    | 2 | Short session ID | `9dc8576c` |
    | 3 | CWD when suggestion fired | `/x/forge/axiom` |
    | 4 | Mutating op count | `12` |
    | 5 | Unique tool type count | `5` |
    | 6 | Total tool calls | `28` |
    | 7 | Top-6 tool histogram | `Write(4) Edit(3) Bash(3)` |
    
    `/sync` reads this log at session start and surfaces any entries from the
    last 72 hours under a **"Skill Suggestions"** section — the one place the
    user will reliably see them.
    
    ### Viewing and clearing
    
    - `auto-skill pending` — `cat ~/.claude/auto-skill/pending.log` (or show
      "no pending suggestions" if absent/empty)
    - `auto-skill clear` — truncate after confirming with the user
    
    ## Per-Project Disable
    
    ```bash
    touch .claude/auto-skill.disable    # Disable Stop hook suggestions
    rm .claude/auto-skill.disable       # Re-enable
    ```
    
    The skill itself can always be invoked manually regardless of this setting.
    
    ## Hook Setup
    
    Auto-skill uses two hooks for automatic suggestions. These are installed globally:
    
    ```
    ~/.claude/auto-skill/
      track-tools.sh     # PostToolUse: counts tool calls per session
      evaluate.sh        # Stop: suggests skill creation if complex enough
    ```
    
    Both hooks fail silently - they will never produce error output or block Claude.
    
    ### Hook Configuration
    
    Add to `~/.claude/settings.json` (merge with existing hooks):
    
    ```json
    {
      "hooks": {
        "PostToolUse": [{
          "matcher": "*",
          "hooks": [{
            "type": "command",
            "command": "bash \"$HOME/.claude/auto-skill/track-tools.sh\"",
            "timeout": 2
          }]
        }],
        "Stop": [{
          "hooks": [{
            "type": "command",
            "command": "bash \"$HOME/.claude/auto-skill/evaluate.sh\"",
            "timeout": 5
          }]
        }]
      }
    }
    ```
    
    ## Suggestion Gates
    
    The Stop hook only suggests skill creation when ALL of these pass:
    
    | Gate | Threshold | Rationale |
    |------|-----------|-----------|
    | **Mutating ops** | 8+ | High bar reduces noise from routine edits |
    | **Tool diversity** | 4+ distinct types | Write+Edit+Bash+Agent = workflow; Write*20 = repetitive |
    | **No non-harness skill loaded** | Skill tool absent OR only harness skills | If following a domain skill, work isn't novel. Harness skills (sync, save, introspect, auto-skill, setperms, tool-discovery) are whitelisted — they're bootstrap/meta, not recipes. |
    | **Per-session** | Once per session | Never nags on resume/continue |
    | **Not disabled** | No `.disable` file | Global or per-project toggle |
    
    Read-only tools (Read, Glob, Grep, LS, Task*) are excluded from counts.
    
    ## Design Decisions
    
    - **Stop hook, not in-loop**: Claude Code doesn't expose the agent loop. The Stop hook fires while context is still in memory, which is the best we can get.
    - **systemMessage output**: The Stop hook outputs JSON that Claude Code displays to the user. Non-blocking, dismissible.
    - **Diversity over volume**: Tool type count matters more than raw call count. A 20-file rename isn't a skill; a workflow using Write+Edit+Bash+Agent probably is.
    - **Per-session cooldown**: Uses a temp file keyed by session ID. No harsh time-based cooldown - each new session gets a fresh chance.
    - **500-line cap on tracking**: Prevents runaway sessions from filling /tmp.
    - **Silent failures**: Both hooks wrap everything in `2>/dev/null` and always `exit 0`.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related