Skip to content
Agent Orchestration
Hook

Hooks

What team-avatar runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
team-avatar
11079 skills10 agents9 commands3 hooks
Install
> /plugin marketplace add tmcfarlane/team-avatar
> /plugin install team-avatar@team-avatar

Ships with team-avatar. Installing the plugin gets these hooks.

Where it lives

  • hooks/guard-shell.shGitHub
    Read the script
    #!/usr/bin/env bash
    # beforeShellExecution hook — guards the agent's shell calls.
    #
    # Reads a JSON payload on stdin (fields: command, cwd, hook_event_name, ...) and prints a
    # JSON decision on stdout per Cursor's hook protocol:
    #   {"continue": <bool>, "permission": "allow|deny|ask", "userMessage": "...", "agentMessage": "..."}
    #
    # Blocks (deny) clearly destructive commands and commits that contain the anti-patterns
    # Team Avatar agents are told never to introduce.
    #
    # Robustness notes (learned from validation on Cursor 3.8.23):
    #  - Cursor runs hooks with the GUI app's PATH, which often LACKS /opt/homebrew/bin, so we
    #    cannot assume `python3` or `jq` are on PATH. We try jq, then python3 at known absolute
    #    paths, then /usr/bin/perl (always present on macOS). If the payload can't be parsed we
    #    return "ask" (surface) rather than silently allowing.
    #  - We cd into the payload's `cwd` before running git checks so `git diff --cached` resolves
    #    in the agent's actual worktree.
    #  - Pair with "failClosed": true in hooks.json so a script error denies instead of allowing.
    #
    # Env switches:
    #   OMC_HOOKS_OBSERVE=1  run non-blocking: downgrade every deny to allow + an informational note.
    #   OMC_HOOKS_DEBUG=1    append each invocation's command to hooks/last-invocation.log (proves firing).
    
    set -uo pipefail
    
    HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    INPUT="$(cat)"
    
    # --- Robust JSON string-field extraction (no reliance on PATH) ---
    json_field() {
      local field="$1" data="$2" out py
      if command -v jq >/dev/null 2>&1; then
        if out="$(printf '%s' "$data" | jq -r --arg f "$field" '.[$f] // empty' 2>/dev/null)"; then
          printf '%s' "$out"; return 0
        fi
      fi
      for py in python3 /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
        if "$py" -c '' >/dev/null 2>&1; then
          if out="$(printf '%s' "$data" | "$py" -c "import sys,json;print(json.load(sys.stdin).get('$field',''))" 2>/dev/null)"; then
            printf '%s' "$out"; return 0
          fi
        fi
      done
      if [ -x /usr/bin/perl ]; then
        printf '%s' "$data" | /usr/bin/perl -0777 -ne 'if(/"'"$field"'"\s*:\s*"((?:[^"\\]|\\.)*)"/s){my $x=$1;$x=~s/\\n/\n/g;$x=~s/\\t/\t/g;$x=~s/\\(.)/$1/g;print $x}' 2>/dev/null
        return 0
      fi
      return 1
    }
    
    COMMAND="$(json_field command "$INPUT")"; PARSE_RC=$?
    CWD="$(json_field cwd "$INPUT")"
    
    if [ "${OMC_HOOKS_DEBUG:-}" = "1" ]; then
      printf '%s\t%s\n' "$(date '+%FT%T' 2>/dev/null)" "${COMMAND:-<unparsed>}" >> "$HERE/last-invocation.log" 2>/dev/null || true
    fi
    
    allow() { printf '{"continue": true, "permission": "allow"}\n'; exit 0; }
    ask() { printf '{"continue": true, "permission": "ask", "userMessage": "%s", "agentMessage": "%s"}\n' "$1" "$1"; exit 0; }
    decide() {
      local reason="$1"
      if [ "${OMC_HOOKS_OBSERVE:-}" = "1" ]; then
        printf '{"continue": true, "permission": "allow", "agentMessage": "[observe] would block: %s"}\n' "$reason"
        exit 0
      fi
      printf '{"continue": false, "permission": "deny", "userMessage": "Blocked by oh-my-cursor: %s", "agentMessage": "Denied by policy: %s. Adjust .cursor/hooks/guard-shell.sh or set OMC_HOOKS_OBSERVE=1 to override."}\n' "$reason" "$reason"
      exit 0
    }
    # Surface for human approval (deterministic "hold for review"). Used where the auto-review
    # classifier is unreliable, e.g. reading credential/secret files.
    hold() {
      local reason="$1"
      if [ "${OMC_HOOKS_OBSERVE:-}" = "1" ]; then
        printf '{"continue": true, "permission": "allow", "agentMessage": "[observe] would hold for review: %s"}\n' "$reason"
        exit 0
      fi
      printf '{"continue": true, "permission": "ask", "userMessage": "Held by oh-my-cursor for review: %s", "agentMessage": "Held for review: %s. Approve only if this credential/secret access is intended."}\n' "$reason" "$reason"
      exit 0
    }
    
    # Received a payload but couldn't parse the command -> surface instead of silently allowing.
    if [ -n "$INPUT" ] && { [ "$PARSE_RC" -ne 0 ] || { [ -z "$COMMAND" ] && printf '%s' "$INPUT" | grep -q '"command"'; }; }; then
      [ "${OMC_HOOKS_OBSERVE:-}" = "1" ] && allow
      ask "oh-my-cursor guard could not parse the shell command; review before running"
    fi
    
    [ -z "$COMMAND" ] && allow
    
    # Resolve git checks in the agent's real worktree.
    if [ -n "$CWD" ] && [ -d "$CWD" ]; then cd "$CWD" 2>/dev/null || true; fi
    
    # --- Destructive command denylist (unambiguous, always guarded) ---
    case "$COMMAND" in
      *"rm -rf /"*|*"rm -rf ~"*|*"rm -rf /*"*|*":(){ :|:& };:"*) decide "destructive filesystem command" ;;
      *"git push"*"--force"*"main"*|*"git push"*"--force"*"master"*|*"git push -f"*"main"*|*"git push -f"*"master"*) decide "force-push to a protected branch" ;;
      *"git reset --hard origin/main"*|*"git reset --hard origin/master"*) decide "hard reset of a shared branch" ;;
      *"chmod -R 777 /"*|*"mkfs"*|*"dd if="*"of=/dev/"*) decide "system-level destructive command" ;;
    esac
    
    # --- Commit anti-pattern guard ---
    case "$COMMAND" in
      *"git commit"*)
        if [ -x "$HERE/pre-commit-check.sh" ]; then
          if ! OUT="$("$HERE/pre-commit-check.sh" 2>&1)"; then
            decide "commit contains forbidden anti-patterns ($(printf '%s' "$OUT" | tr '\n' ' ' | cut -c1-160))"
          fi
        fi
        ;;
    esac
    
    # --- Credential / secret access guard (hold for review) ---
    # The permissions.json auto-review classifier is best-effort and let `cat ~/.ssh/config`
    # through during E2E test C2, so enforce a deterministic hold here regardless of the classifier.
    case "$COMMAND" in
      *"/.ssh/"*|*"/.aws/"*|*"/.gnupg/"*|*"id_rsa"*|*"id_dsa"*|*"id_ecdsa"*|*"id_ed25519"*|*".pem"*|*".netrc"*|*".pgpass"*|*"/.kube/config"*|*"kubeconfig"*|*"/.docker/config.json"*|*"/.config/gcloud/"*|*".aws/credentials"*)
        hold "command accesses a credential/secret file" ;;
    esac
    
    allow
    
  • hooks/post-edit-lint.shGitHub
    Read the script
    #!/usr/bin/env bash
    # afterFileEdit hook — lints a file right after the agent edits it.
    #
    # Reads a JSON payload on stdin (fields: file_path, edits, hook_event_name, ...).
    # afterFileEdit is INFORMATIONAL: Cursor ignores the output, so this hook only surfaces
    # lint findings; it cannot (and should not) block the edit.
    #
    # Uses the same PATH-robust JSON parsing as guard-shell.sh (Cursor runs hooks with the GUI
    # app PATH, which may lack python3/jq). Set OMC_HOOKS_DEBUG=1 to log invocations.
    
    set -uo pipefail
    
    HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    INPUT="$(cat)"
    
    json_field() {
      local field="$1" data="$2" out py
      if command -v jq >/dev/null 2>&1; then
        if out="$(printf '%s' "$data" | jq -r --arg f "$field" '.[$f] // empty' 2>/dev/null)"; then
          printf '%s' "$out"; return 0
        fi
      fi
      for py in python3 /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
        if "$py" -c '' >/dev/null 2>&1; then
          if out="$(printf '%s' "$data" | "$py" -c "import sys,json;print(json.load(sys.stdin).get('$field',''))" 2>/dev/null)"; then
            printf '%s' "$out"; return 0
          fi
        fi
      done
      if [ -x /usr/bin/perl ]; then
        printf '%s' "$data" | /usr/bin/perl -0777 -ne 'if(/"'"$field"'"\s*:\s*"((?:[^"\\]|\\.)*)"/s){my $x=$1;$x=~s/\\n/\n/g;$x=~s/\\(.)/$1/g;print $x}' 2>/dev/null
        return 0
      fi
      return 1
    }
    
    FILE="$(json_field file_path "$INPUT")"
    
    if [ "${OMC_HOOKS_DEBUG:-}" = "1" ]; then
      printf '%s\tedit\t%s\n' "$(date '+%FT%T' 2>/dev/null)" "${FILE:-<unparsed>}" >> "$HERE/last-invocation.log" 2>/dev/null || true
    fi
    
    [ -z "$FILE" ] && exit 0
    [ -f "$FILE" ] || exit 0
    
    ext="${FILE##*.}"
    
    case "$ext" in
      ts|tsx|js|jsx)
        command -v npx >/dev/null 2>&1 && npx eslint --no-error-on-unmatched-pattern "$FILE" 2>/dev/null || true
        ;;
      py)
        if command -v ruff >/dev/null 2>&1; then
          ruff check "$FILE" 2>/dev/null || true
        elif command -v flake8 >/dev/null 2>&1; then
          flake8 "$FILE" 2>/dev/null || true
        fi
        ;;
      rs)
        command -v cargo >/dev/null 2>&1 && cargo clippy --message-format=short 2>/dev/null || true
        ;;
      go)
        command -v golangci-lint >/dev/null 2>&1 && golangci-lint run "$FILE" 2>/dev/null || true
        ;;
    esac
    
    exit 0
    
  • hooks/pre-commit-check.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Pre-commit hook: enforce hard constraints before any commit.
    # Place in .cursor/hooks/ and configure via Cursor's hook system.
    #
    # Catches anti-patterns that agents are instructed never to use,
    # providing system-level enforcement as a safety net.
    
    set -euo pipefail
    
    STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
    
    if [ -z "$STAGED_FILES" ]; then
      exit 0
    fi
    
    VIOLATIONS=0
    
    check_pattern() {
      local pattern="$1"
      local label="$2"
      local file="$3"
    
      if grep -qn "$pattern" "$file" 2>/dev/null; then
        echo "VIOLATION in $file: $label"
        grep -n "$pattern" "$file" 2>/dev/null | head -5
        echo ""
        VIOLATIONS=$((VIOLATIONS + 1))
      fi
    }
    
    for file in $STAGED_FILES; do
      if [ ! -f "$file" ]; then
        continue
      fi
    
      ext="${file##*.}"
    
      case "$ext" in
        ts|tsx|js|jsx)
          check_pattern "as any" "Type safety: 'as any' suppresses type checking" "$file"
          check_pattern "@ts-ignore" "Type safety: '@ts-ignore' suppresses errors" "$file"
          check_pattern "@ts-expect-error" "Type safety: '@ts-expect-error' suppresses errors" "$file"
          check_pattern "catch.*{[[:space:]]*}" "Error handling: empty catch block" "$file"
          ;;
        py)
          check_pattern "except:$" "Error handling: bare except clause" "$file"
          check_pattern "pass$" "Error handling: potential empty except/pass" "$file"
          ;;
      esac
    done
    
    if [ "$VIOLATIONS" -gt 0 ]; then
      echo "Found $VIOLATIONS constraint violation(s). Fix before committing."
      exit 1
    fi
    

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 withteam-avatar

Team Avatar: a universal agent orchestrator for Claude Code, Codex, and Cursor. Nine Avatar characters, 58 engineering skills. pstack with Avatar characters on top.

Get the whole plugin