Agent Orchestration
Hook
Hooks
What model-matchmaker runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add coyvalyss1/model-matchmaker --agent claude-codeShips with model-matchmaker. Installing the plugin gets these hooks.
Where it lives
- hooks/analytics.shGitHub
Read the script
#!/bin/bash # Model Matchmaker Analytics # Parses NDJSON logs and prints accuracy/usage metrics. # Usage: ./hooks/analytics.sh [--json] [--days N] LOG_PATH="${HOME}/.cursor/hooks/model-matchmaker.ndjson" if [ ! -f "$LOG_PATH" ]; then echo "No analytics data found at $LOG_PATH" echo "Start using Model Matchmaker and data will be collected automatically." exit 0 fi DAYS="${2:-0}" JSON_OUT=false [ "$1" = "--json" ] && JSON_OUT=true && DAYS="${3:-0}" [ "$1" = "--days" ] && DAYS="$2" python3 -c " import json, sys from datetime import datetime, timedelta from collections import Counter, defaultdict log_path = '$LOG_PATH' days_filter = int('$DAYS') json_out = $([[ "$JSON_OUT" == "true" ]] && echo "True" || echo "False") recs = [] completions = [] cutoff = None if days_filter > 0: cutoff = datetime.now() - timedelta(days=days_filter) with open(log_path) as f: for line in f: line = line.strip() if not line: continue try: entry = json.loads(line) except: continue if cutoff: ts = datetime.fromisoformat(entry.get('ts', '')) if ts < cutoff: continue if entry.get('event') == 'recommendation': recs.append(entry) elif entry.get('event') == 'completion': completions.append(entry) total_recs = len(recs) if total_recs == 0: print('No recommendation data found.' if not json_out else json.dumps({'error': 'no data'})) sys.exit(0) actions = Counter(r['action'] for r in recs) rec_types = Counter(r['recommendation'] for r in recs) models_used = Counter(r['model'] for r in recs) overrides = [r for r in recs if r['action'] == 'OVERRIDE'] blocks = [r for r in recs if r['action'] == 'BLOCK'] allows = [r for r in recs if r['action'] == 'ALLOW'] override_rate = len(overrides) / total_recs * 100 if total_recs else 0 block_rate = len(blocks) / total_recs * 100 if total_recs else 0 # Override analysis: what was recommended vs what model was used override_details = Counter() for o in overrides: override_details[f'{o[\"model\"]} (rec: {o[\"recommendation\"]})'] += 1 # Block analysis: what model was being used vs recommendation block_details = Counter() for b in blocks: block_details[f'{b[\"model\"]} -> {b[\"recommendation\"]}'] += 1 # Completion analysis: correlate recommendations with outcomes completion_by_conv = {} for c in completions: cid = c.get('conversation_id', '') if cid: completion_by_conv[cid] = c rec_by_conv = defaultdict(list) for r in recs: cid = r.get('conversation_id', '') if cid: rec_by_conv[cid].append(r) completed_convs = 0 errored_convs = 0 aborted_convs = 0 override_completed = 0 override_errored = 0 for cid, comp in completion_by_conv.items(): status = comp.get('status', '') had_override = any(r['action'] == 'OVERRIDE' for r in rec_by_conv.get(cid, [])) if status == 'completed': completed_convs += 1 if had_override: override_completed += 1 elif status == 'error': errored_convs += 1 if had_override: override_errored += 1 elif status == 'aborted': aborted_convs += 1 if json_out: print(json.dumps({ 'total_recommendations': total_recs, 'actions': dict(actions), 'recommendations': dict(rec_types), 'models_used': dict(models_used), 'override_rate': round(override_rate, 1), 'block_rate': round(block_rate, 1), 'override_details': dict(override_details), 'block_details': dict(block_details), 'completions': { 'completed': completed_convs, 'errored': errored_convs, 'aborted': aborted_convs, }, 'override_outcomes': { 'completed': override_completed, 'errored': override_errored, }, }, indent=2)) else: print('=' * 50) print(' MODEL MATCHMAKER ANALYTICS') period = f' (last {days_filter} days)' if days_filter > 0 else '' print(f' {total_recs} recommendations tracked{period}') print('=' * 50) print(f'\n Actions:') for action, count in actions.most_common(): pct = count / total_recs * 100 print(f' {action:>10}: {count:>4} ({pct:.1f}%)') print(f'\n Override rate: {override_rate:.1f}%') print(f' Block rate: {block_rate:.1f}%') if override_details: print(f'\n Override breakdown (you disagreed):') for detail, count in override_details.most_common(): print(f' {detail}: {count}') if block_details: print(f'\n Block breakdown (model mismatch):') for detail, count in block_details.most_common(): print(f' {detail}: {count}') print(f'\n Recommendations:') for rec, count in rec_types.most_common(): pct = count / total_recs * 100 print(f' {rec:>10}: {count:>4} ({pct:.1f}%)') print(f'\n Models used:') for model, count in models_used.most_common(): print(f' {model}: {count}') total_completions = completed_convs + errored_convs + aborted_convs if total_completions > 0: print(f'\n Task Outcomes ({total_completions} conversations):') print(f' Completed: {completed_convs}') print(f' Errored: {errored_convs}') print(f' Aborted: {aborted_convs}') if override_completed + override_errored > 0: print(f'\n Override Outcomes:') print(f' Completed after override: {override_completed}') print(f' Errored after override: {override_errored}') # Auto-switch impact tracking auto_switched = [e for e in recs if e.get('auto_switch_attempted') == True] manual_blocks = [e for e in blocks if not e.get('auto_switch_attempted')] if auto_switched: print(f'\n Auto-Switch Impact:') print(f' Blocks with auto-switch: {len(auto_switched)}') print(f' Manual blocks: - hooks/auto-switch-model.shGitHub
Read the script
#!/bin/bash # Auto-Switch Model via Keyboard Automation # Uses Cmd+/ to open model dropdown, then arrow keys to navigate to target model. # # Cursor's official keyboard shortcuts: # - Cmd+/: Loop between AI models (opens dropdown) # - Arrow keys + Enter: Navigate and select # # Proxy approach: Cursor's subprocess doesn't have macOS Accessibility permissions, # so we write a temp script and execute via Terminal.app (which does have them). # # Usage: ./auto-switch-model.sh <model_name> [--test] # Example: ./auto-switch-model.sh "sonnet" # Test mode: ./auto-switch-model.sh "sonnet" --test (bypasses enable check) MODEL="$1" TEST_MODE="${2:-}" LOG_FILE="$HOME/.cursor/hooks/auto-switch-audit.log" CIRCUIT_FILE="$HOME/.cursor/hooks/.auto-switch-circuit" LOCK_FILE="$HOME/.cursor/hooks/.auto-switch.lock" KILL_SWITCH="$HOME/.cursor/hooks/.auto-switch-kill" AUTO_SWITCH_FLAG="$HOME/.cursor/hooks/.auto-switch-enabled" # SECURITY: Emergency kill switch if [ -f "$KILL_SWITCH" ]; then echo "[$(date -Iseconds)] SECURITY: Kill switch activated" >> "$LOG_FILE" exit 0 fi # SECURITY: Check if auto-switch is enabled (skip in test mode) if [ "$TEST_MODE" != "--test" ] && [ ! -f "$AUTO_SWITCH_FLAG" ]; then exit 0 fi # SECURITY: Input validation - only allow whitelisted model names case "$MODEL" in haiku|sonnet|opus) ;; *) echo "[$(date -Iseconds)] SECURITY: Invalid model rejected: $MODEL" >> "$LOG_FILE" exit 1 ;; esac # SECURITY: Execution lock - prevent simultaneous executions if [ -f "$LOCK_FILE" ]; then LOCK_AGE=$(( $(date +%s) - $(stat -f %m "$LOCK_FILE" 2>/dev/null || echo 0) )) if [ $LOCK_AGE -lt 10 ]; then echo "[$(date -Iseconds)] SECURITY: Lock exists, switch in progress" >> "$LOG_FILE" exit 0 else rm -f "$LOCK_FILE" fi fi touch "$LOCK_FILE" trap "rm -f $LOCK_FILE" EXIT # SECURITY: Rate limiting - prevent rapid-fire switching CURRENT_TIME=$(date +%s) if [ -f "$CIRCUIT_FILE" ]; then LAST_SWITCH=$(cat "$CIRCUIT_FILE" 2>/dev/null || echo 0) TIME_DIFF=$((CURRENT_TIME - LAST_SWITCH)) if [ $TIME_DIFF -lt 5 ]; then echo "[$(date -Iseconds)] SECURITY: Rate limit - too soon since last switch (${TIME_DIFF}s)" >> "$LOG_FILE" exit 0 fi fi echo "$CURRENT_TIME" > "$CIRCUIT_FILE" # Detect current Cursor mode (affects dropdown positions) MODE_FILE="$HOME/.cursor/hooks/.cursor-mode" CURSOR_MODE="agent" # default if [ -f "$MODE_FILE" ]; then CURSOR_MODE=$(cat "$MODE_FILE" 2>/dev/null || echo "agent") fi # Window targeting from environment (set by model-advisor.sh) WINDOW_TITLE="${WINDOW_TITLE:-}" CONVERSATION_ID="${CONVERSATION_ID:-}" # Audit log echo "[$(date -Iseconds)] SWITCH_ATTEMPT | Model: $MODEL | Mode: $CURSOR_MODE | Window: $WINDOW_TITLE | PID: $$ | User: $(whoami)" >> "$LOG_FILE" # Search-based model selection: type model name into dropdown search bar SEARCH_TERM="$MODEL" if [ "$CURSOR_MODE" = "plan" ] && [ "$MODEL" = "haiku" ]; then echo "[$(date -Iseconds)] INFO | Haiku not available in Plan mode, switching to Sonnet" >> "$LOG_FILE" MODEL="sonnet" SEARCH_TERM="sonnet" fi # Proxy via Terminal.app to get Accessibility permissions # Cursor's subprocess doesn't inherit macOS Accessibility/Automation permissions, # so we write a temp script and execute it through Terminal.app which has them. PROXY_SCRIPT="/tmp/.cursor-model-switch-$$.sh" RESULT_FILE="/tmp/.cursor-model-switch-$$-result.txt" cat > "$PROXY_SCRIPT" << PROXY_EOF #!/bin/bash # Capture the window title to target this specific Cursor window WINDOW_TITLE="$WINDOW_TITLE" osascript - "$MODEL" "$SEARCH_TERM" "\$WINDOW_TITLE" <<'APPLESCRIPT_EOF' on run argv set targetModel to item 1 of argv set searchTerm to item 2 of argv set targetWindow to item 3 of argv -- Activate Cursor (needed because script runs via Terminal.app) tell application "Cursor" activate end tell -- Brief wait for Cursor to come to front delay 0.5 -- Verify Cursor is frontmost tell application "System Events" if frontmost of process "Cursor" is false then do shell script "echo ABORTED > $RESULT_FILE" return "ABORTED: Cursor not frontmost" end if end tell -- Open model dropdown with Cmd+/, then type to search tell application "System Events" tell process "Cursor" -- Open dropdown keystroke "/" using command down delay 1.5 -- Use keystroke (not key code) to type directly into focused field -- keystroke respects the currently focused element keystroke searchTerm delay 1.5 -- Enter to select model key code 36 end tell end tell return "SUCCESS" end run APPLESCRIPT_EOF echo "EXIT:\$?" > "$RESULT_FILE" # Self-destruct: delete script and close Terminal window # AppleScript above takes ~4s total - wait until it's fully done sleep 0.2 rm -f "$PROXY_SCRIPT" osascript -e 'tell application "Terminal" to close front window' 2>/dev/null & PROXY_EOF chmod +x "$PROXY_SCRIPT" # Execute through Terminal.app # Uses 'open' which doesn't require Apple Event permissions open -a Terminal "$PROXY_SCRIPT" # Wait for result (up to 8 seconds) for i in $(seq 1 16); do if [ -f "$RESULT_FILE" ]; then RESULT=$(cat "$RESULT_FILE") rm -f "$PROXY_SCRIPT" "$RESULT_FILE" if echo "$RESULT" | grep -q "EXIT:0"; then echo "[$(date -Iseconds)] SUCCESS | Model: $MODEL" >> "$LOG_FILE" else echo "[$(date -Iseconds)] FAILED | Model: $MODEL | Result: $RESULT" >> "$LOG_FILE" fi exit 0 fi sleep 0.5 done echo "[$(date -Iseconds)] TIMEOUT | Model: $MODEL" >> "$LOG_FILE" rm -f "$PROXY_SCRIPT" "$RESULT_FILE" # Close any orphaned Terminal window from the failed proxy script osascript -e 'tell application "Terminal - hooks/cc-auto-switch-ide.shGitHub
Read the script
#!/usr/bin/env bash # cc-auto-switch-ide.sh — Auto-switch model and re-send prompt in Claude Code IDE (Tier 3) # # Spawned by cc-ide-advisor.sh when auto-switch is enabled and an expensive mismatch is detected. # Uses AppleScript to type /model command and paste the prompt back into Claude Code webview. # # Usage: ./cc-auto-switch-ide.sh <model_name> <prompt_text> # Example: ./cc-auto-switch-ide.sh "haiku" "list all TODO comments" MODEL="$1" PROMPT="$2" LOG_FILE="$HOME/.claude/hooks/mm-audit.log" KILL_SWITCH="$HOME/.claude/hooks/.mm-kill" # SECURITY: Emergency kill switch if [ -f "$KILL_SWITCH" ]; then echo "[$(date -Iseconds)] SECURITY: Kill switch activated" >> "$LOG_FILE" exit 0 fi # SECURITY: Input validation - only allow whitelisted model names case "$MODEL" in haiku|sonnet|opus) ;; *) echo "[$(date -Iseconds)] SECURITY: Invalid model rejected: $MODEL" >> "$LOG_FILE" exit 1 ;; esac # Audit log echo "[$(date -Iseconds)] IDE_AUTO_SWITCH | Model: $MODEL | PID: $$ | User: $(whoami)" >> "$LOG_FILE" # Wait for block message to render sleep 0.7 # Copy prompt to clipboard (backup in case AppleScript fails) echo "$PROMPT" | pbcopy 2>/dev/null # Execute AppleScript automation osascript <<EOF tell application "System Events" tell process "Cursor" -- Focus Claude Code input (Esc key) key code 53 delay 0.4 -- Type /model command keystroke "/model $MODEL" delay 0.2 key code 36 -- Enter delay 1.3 -- Refocus input key code 53 delay 0.4 -- Paste saved prompt keystroke "v" using command down delay 0.4 -- Submit key code 36 -- Enter end tell end tell EOF APPLESCRIPT_EXIT=$? if [ $APPLESCRIPT_EXIT -eq 0 ]; then echo "[$(date -Iseconds)] IDE_AUTO_SWITCH_SUCCESS | Model: $MODEL" >> "$LOG_FILE" else echo "[$(date -Iseconds)] IDE_AUTO_SWITCH_FAILED | Model: $MODEL | Exit: $APPLESCRIPT_EXIT" >> "$LOG_FILE" fi exit 0 - hooks/cc-effort-advisor.shGitHub
Read the script
#!/usr/bin/env bash # cc-effort-advisor.sh — Silent PreToolUse hook for Claude Code # # Fires on Agent tool calls only. Classifies the subagent prompt via # cc-config.json regex rules. Outputs NOTHING to Claude (zero token overhead). # Writes classification result to audit log for analytics only. # # Wired into ~/.claude/settings.json PreToolUse with matcher: Agent # # For all non-Agent tools: immediate silent exit (no overhead at all). KILL_SWITCH="$HOME/.claude/hooks/.mm-kill" CONFIG="$HOME/.claude/hooks/cc-config.json" AUDIT_LOG="$HOME/.claude/hooks/mm-audit.log" # --- Kill switch: silent exit --- [ -f "$KILL_SWITCH" ] && exit 0 # --- Read hook input --- INPUT=$(cat) # --- Only act on Agent tool calls --- TOOL=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null) [ "$TOOL" != "Agent" ] && exit 0 # --- Dependency check: if jq missing, silent exit --- command -v jq &>/dev/null || exit 0 [ -f "$CONFIG" ] || exit 0 # --- Extract subagent prompt --- PROMPT=$(echo "$INPUT" | python3 -c " import sys, json d = json.load(sys.stdin) inp = d.get('tool_input', {}) if isinstance(inp, dict): print(inp.get('prompt', '')) else: print('') " 2>/dev/null) [ -z "$PROMPT" ] && exit 0 # --- Classify prompt against config rules --- DEFAULT_MODEL=$(jq -r '.default.model // "sonnet"' "$CONFIG") DEFAULT_EFFORT=$(jq -r '.default.effort // "low"' "$CONFIG") MATCHED_MODEL="$DEFAULT_MODEL" MATCHED_EFFORT="$DEFAULT_EFFORT" MATCHED_RULE="default" RULE_COUNT=$(jq '.rules | length' "$CONFIG") for i in $(seq 0 $((RULE_COUNT - 1))); do PATTERN=$(jq -r ".rules[$i].pattern" "$CONFIG") NAME=$(jq -r ".rules[$i].name" "$CONFIG") if echo "$PROMPT" | grep -qiE "$PATTERN"; then MATCHED_MODEL=$(jq -r ".rules[$i].model" "$CONFIG") MATCHED_EFFORT=$(jq -r ".rules[$i].effort" "$CONFIG") MATCHED_RULE="$NAME" break fi done # --- Log to audit (no stdout — zero token overhead) --- TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null) PROMPT_SNIPPET="${PROMPT:0:60}" echo "{\"ts\":\"$TS\",\"event\":\"subagent\",\"session_id\":\"$SESSION_ID\",\"rule\":\"$MATCHED_RULE\",\"model\":\"$MATCHED_MODEL\",\"effort\":\"$MATCHED_EFFORT\",\"prompt\":\"$PROMPT_SNIPPET\"}" >> "$AUDIT_LOG" # --- Silent exit: output nothing to Claude --- exit 0 - hooks/cc-ide-advisor.shGitHub
Read the script
#!/usr/bin/env bash # cc-ide-advisor.sh — UserPromptSubmit hook for Claude Code IDE sessions (Three-Tier) # # Fires on every user message in the IDE. Classifies the prompt via cc-config.json # and uses a three-tier approach for cost optimization: # # Tier 1 (Silent): Pass through with effortLevel optimization when cost delta is small # Tier 2 (Clipboard): Block + copy prompt to clipboard for expensive mismatches # Tier 3 (Auto): Full automation when auto-switch is enabled (opt-in) # # Supports +/++ prefix overrides: "+fix bug" or "++prove theorem" # Supports ! prefix to bypass blocking entirely # # Wired into ~/.claude/settings.json UserPromptSubmit hook. # Paired with cc-ide-restore.sh (Stop hook) which resets to defaults after each response. KILL_SWITCH="$HOME/.claude/hooks/.mm-kill" CONFIG="$HOME/.claude/hooks/cc-config.json" SETTINGS="$HOME/.claude/settings.json" AUDIT_LOG="$HOME/.claude/hooks/mm-audit.log" AUTO_SWITCH_FLAG="$HOME/.claude/hooks/.mm-auto-switch-enabled" PENDING_PROMPT_FILE="$HOME/.claude/hooks/.pending-prompt.txt" # --- Kill switch: silent exit --- [ -f "$KILL_SWITCH" ] && exit 0 # --- Dependency check --- command -v jq &>/dev/null || exit 0 [ -f "$CONFIG" ] || exit 0 [ -f "$SETTINGS" ] || exit 0 # --- Read hook input --- INPUT=$(cat) # --- Extract user prompt --- PROMPT=$(echo "$INPUT" | python3 -c " import sys, json d = json.load(sys.stdin) print(d.get('prompt', '')) " 2>/dev/null) # --- Handle ! override prefix (bypass all blocking) --- if [[ "$PROMPT" == "!"* ]]; then # Strip prefix and pass through PROMPT="${PROMPT:1}" IS_OVERRIDE=true else IS_OVERRIDE=false fi # --- Handle slash commands (pass through without classification) --- if [[ "$PROMPT" == "/"* ]]; then # Slash command like /model or /compact — pass through immediately exit 0 fi # --- Load defaults from config --- DEFAULT_MODEL=$(jq -r '.default.model // "sonnet"' "$CONFIG") DEFAULT_EFFORT=$(jq -r '.default.effort // "low"' "$CONFIG") DEFAULT_THINKING=$(jq -r '.default.thinking // false' "$CONFIG") MATCHED_MODEL="$DEFAULT_MODEL" MATCHED_EFFORT="$DEFAULT_EFFORT" MATCHED_THINKING="$DEFAULT_THINKING" MATCHED_RULE="default" # --- Override prefix detection (++ before +) --- # ++ = opus + high + thinking on # + = opus + medium + thinking off if [[ "$PROMPT" == "++"* ]]; then MATCHED_MODEL="opus" MATCHED_EFFORT="high" MATCHED_THINKING="true" MATCHED_RULE="override:++" elif [[ "$PROMPT" == "+"* ]]; then MATCHED_MODEL="opus" MATCHED_EFFORT="medium" MATCHED_THINKING="false" MATCHED_RULE="override:+" else # --- Classify prompt against config rules (top-to-bottom, first match wins) --- if [ -n "$PROMPT" ]; then RULE_COUNT=$(jq '.rules | length' "$CONFIG") for i in $(seq 0 $((RULE_COUNT - 1))); do PATTERN=$(jq -r ".rules[$i].pattern" "$CONFIG") NAME=$(jq -r ".rules[$i].name" "$CONFIG") if echo "$PROMPT" | grep -qiE "$PATTERN"; then MATCHED_MODEL=$(jq -r ".rules[$i].model" "$CONFIG") MATCHED_EFFORT=$(jq -r ".rules[$i].effort" "$CONFIG") MATCHED_THINKING=$(jq -r ".rules[$i].thinking" "$CONFIG") MATCHED_RULE="$NAME" break fi done fi fi # --- Map short alias to full model ID --- case "$MATCHED_MODEL" in haiku) FULL_MODEL="claude-haiku-4-5-20251001" ;; sonnet) FULL_MODEL="claude-sonnet-4-6" ;; opus) FULL_MODEL="claude-opus-4-6" ;; *) FULL_MODEL="$MATCHED_MODEL" ;; esac # --- Read current session model from settings.json --- CURRENT_MODEL=$(jq -r '.model // "claude-sonnet-4-6"' "$SETTINGS") # Extract short alias from current model case "$CURRENT_MODEL" in *haiku*) CURRENT_ALIAS="haiku" ;; *opus*) CURRENT_ALIAS="opus" ;; *) CURRENT_ALIAS="sonnet" ;; esac # --- Three-Tier Decision Logic --- # If ! override prefix, pass through regardless of mismatch if [ "$IS_OVERRIDE" = true ]; then TIER="override" ACTION="pass-through" # If models match, pass through silently elif [ "$MATCHED_MODEL" = "$CURRENT_ALIAS" ]; then TIER="match" ACTION="pass-through" # Expensive mismatches: opus → haiku or opus → sonnet (save $0.80-5.00) elif [ "$CURRENT_ALIAS" = "opus" ] && [ "$MATCHED_MODEL" != "opus" ]; then TIER="expensive-mismatch" ACTION="block" # Upward mismatches: haiku/sonnet → higher tier (add hint but pass through) elif [ "$CURRENT_ALIAS" = "haiku" ] && [ "$MATCHED_MODEL" = "sonnet" ]; then TIER="upward-hint" ACTION="pass-through-with-hint" elif [ "$CURRENT_ALIAS" = "haiku" ] && [ "$MATCHED_MODEL" = "opus" ]; then TIER="upward-hint" ACTION="pass-through-with-hint" elif [ "$CURRENT_ALIAS" = "sonnet" ] && [ "$MATCHED_MODEL" = "opus" ]; then TIER="upward-hint" ACTION="pass-through-with-hint" # Small overpay: sonnet → haiku task (overpay ~$0.03-0.10, accept for zero friction) elif [ "$CURRENT_ALIAS" = "sonnet" ] && [ "$MATCHED_MODEL" = "haiku" ]; then TIER="small-overpay" ACTION="pass-through" # Default: pass through else TIER="unknown" ACTION="pass-through" fi # --- Write effort + thinking to settings.json (always, even when blocking) --- TMP=$(mktemp) jq --arg model "$CURRENT_MODEL" \ --arg effort "$MATCHED_EFFORT" \ --argjson thinking "$MATCHED_THINKING" \ '.model = $model | .effortLevel = $effort | .alwaysThinkingEnabled = $thinking' \ "$SETTINGS" > "$TMP" && mv "$TMP" "$SETTINGS" # --- Audit log --- TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null) PROMPT_SNIPPET="${PROMPT:0:60}" echo "{\"ts\":\"$TS\",\"event\":\"ide-classify\",\"session_id\":\"$SESSION_ID\",\"rule\":\"$MATCHED_RULE\",\"recommended\":\"$MATCHED_MODEL\",\"current\":\"$CURRENT_ALIAS\",\"tier\":\"$TIER\",\"action\":\"$ACTION\",\"effort\":\"$MATCHED_EFFORT\",\"thinking\":$MATCHED_THINKING,\"prompt\":\"$PROMPT_SNIPPET\"}" >> "$AUDIT_LOG" # --- Execute action based on tier --- if [ "$ACTION" = "block" ]; then # Tier 2/3: Expensive mismatch — bloc - hooks/cc-ide-restore.shGitHub
Read the script
#!/usr/bin/env bash # cc-ide-restore.sh — Stop hook for Claude Code IDE sessions # # Fires when Claude finishes responding. Resets model + effortLevel + # alwaysThinkingEnabled back to the defaults in cc-config.json. # # This ensures that a +/++ override or a deep-work classification doesn't # persist into the next message (each message gets freshly classified). # # Wired into ~/.claude/settings.json Stop hook. KILL_SWITCH="$HOME/.claude/hooks/.mm-kill" CONFIG="$HOME/.claude/hooks/cc-config.json" SETTINGS="$HOME/.claude/settings.json" # --- Kill switch: silent exit --- [ -f "$KILL_SWITCH" ] && exit 0 # --- Dependency check --- command -v jq &>/dev/null || exit 0 [ -f "$CONFIG" ] || exit 0 [ -f "$SETTINGS" ] || exit 0 # --- Read defaults from config --- DEFAULT_MODEL=$(jq -r '.default.model // "sonnet"' "$CONFIG") DEFAULT_EFFORT=$(jq -r '.default.effort // "low"' "$CONFIG") DEFAULT_THINKING=$(jq -r '.default.thinking // false' "$CONFIG") # --- Map short alias to full model ID --- case "$DEFAULT_MODEL" in haiku) FULL_DEFAULT="claude-haiku-4-5-20251001" ;; sonnet) FULL_DEFAULT="claude-sonnet-4-6" ;; opus) FULL_DEFAULT="claude-opus-4-6" ;; *) FULL_DEFAULT="$DEFAULT_MODEL" ;; esac # --- Restore settings.json to defaults --- TMP=$(mktemp) jq --arg model "$FULL_DEFAULT" \ --arg effort "$DEFAULT_EFFORT" \ --argjson thinking "$DEFAULT_THINKING" \ '.model = $model | .effortLevel = $effort | .alwaysThinkingEnabled = $thinking' \ "$SETTINGS" > "$TMP" && mv "$TMP" "$SETTINGS" exit 0
- hooks/cc-launch.shGitHub
- hooks/export-analytics-for-automation.shGitHub
- hooks/log-cursor-mode.shGitHub
- hooks/mm-control.shGitHub
- hooks/model-advisor.shGitHub
- hooks/session-init.shGitHub
- hooks/toggle-auto-switch.shGitHub
- hooks/track-completion.shGitHub
All 14 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withmodel-matchmaker
Stop paying Opus prices to rename files. Stop waiting 20 seconds for responses that should take 3. A local hook for Cursor and Claude Code that classifies every prompt before it's sent and recommends the right model.
Get the whole plugin
Stats
168
Stars
15
Forks
Active
Maintenance
Shell
Language
MIT
License
29d ago
Last commit
7mo ago
Created
4h ago
Added

