Skip to content
Development
Hook

Hooks

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

From plugin
agy-delegate
172 skills2 agents5 commands1 hook
Install
> /plugin marketplace add davdittrich/delegate-agy
> /plugin install agy-delegate@agy-delegate

Ships with agy-delegate. Installing the plugin gets these hooks.

What fires, and when

SubagentStart

  • ${CLAUDE_PLUGIN_ROOT}/hooks/agy-subagent-policy.sh
Read hooks/hooks.json

Where it lives

  • hooks/agy-hooks-lib.shGitHub
    Read the script
    # agy-hooks-lib.sh
    #
    # Sourced-only bash function library for agy delegation hooks.
    # This file MUST NOT be executed directly and MUST NOT produce any
    # top-level side effects (no output, no `set -e`/`set -u` leakage into
    # the sourcing shell, no unguarded double-source work).
    #
    # Usage: source "$(dirname "${BASH_SOURCE[0]}")/agy-hooks-lib.sh"
    
    # Include guard: sourcing this file twice must be a cheap no-op.
    if [ -n "${_AGY_HOOKS_LIB_SOURCED:-}" ]; then
      return 0
    fi
    _AGY_HOOKS_LIB_SOURCED=1
    
    # agy_hooks_enabled
    #   Returns 0 (true) iff AGY_HOOKS_ENABLED is one of: 1, true, on, yes
    #   (case-insensitive). Default (unset/anything else) is disabled.
    agy_hooks_enabled() {
      local v="${AGY_HOOKS_ENABLED:-}"
      v="$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')"
      case "$v" in
        1|true|on|yes)
          return 0
          ;;
        *)
          return 1
          ;;
      esac
    }
    
    # agy_hooks_agent_allowed <agent_type>
    #   Returns 0 iff <agent_type> is allowed per AGY_HOOKS_AGENT_TYPES (CSV).
    #   Entries are trimmed of leading/trailing whitespace before comparison.
    #   Matching is exact and case-sensitive:
    #     - A namespaced entry (contains ':') matches ONLY an exact full-string
    #       equal agent_type (no wildcarding).
    #     - A bare entry (no ':') matches an agent_type that is exactly equal
    #       to it, OR a namespaced agent_type whose suffix after the last ':'
    #       equals it (bare entries act as a cross-namespace suffix wildcard).
    agy_hooks_agent_allowed() {
      local agent_type="$1"
      local list="${AGY_HOOKS_AGENT_TYPES:-general-purpose,Explore,metaswarm:researcher-agent,metaswarm:coder-agent,metaswarm:code-review-agent}"
      local -a entries
      IFS=',' read -ra entries <<< "$list"
    
      local entry suffix
      for entry in "${entries[@]}"; do
        # trim leading whitespace
        entry="${entry#"${entry%%[![:space:]]*}"}"
        # trim trailing whitespace
        entry="${entry%"${entry##*[![:space:]]}"}"
    
        [ -z "$entry" ] && continue
    
        if [[ "$entry" == *:* ]]; then
          # Namespaced entry: exact full-string match only, no wildcarding.
          if [ "$entry" = "$agent_type" ]; then
            return 0
          fi
        else
          # Bare entry: exact match, or suffix-after-last-colon match.
          if [ "$entry" = "$agent_type" ]; then
            return 0
          fi
          suffix="${agent_type##*:}"
          if [ "$entry" = "$suffix" ]; then
            return 0
          fi
        fi
      done
    
      return 1
    }
    
    # agy_hooks_parse_field <json_string> <field>
    #   Echoes the top-level string value of <field> from <json_string>.
    #   Echoes an empty string if the field is missing, not a string, or the
    #   JSON is invalid. All parsing goes through python3's json module
    #   (never eval). The python process always exits 0.
    agy_hooks_parse_field() {
      local json_string="$1"
      local field="$2"
    
      printf '%s' "$json_string" | python3 -c '
    import sys, json
    
    data = sys.stdin.read()
    field = sys.argv[1]
    try:
        obj = json.loads(data)
        v = obj.get(field, "")
        print(v if isinstance(v, str) else "")
    except Exception:
        print("")
    ' "$field"
    }
    
    # agy_hooks_debug <reason>
    #   If AGY_HOOKS_DEBUG=1, writes exactly one timestamped line containing
    #   <reason> to $AGY_HOOKS_DEBUG_FILE (append) if set, else to stderr.
    #   Never includes prompt/prompt-id content (callers must not pass any).
    #   No-op if debug is off.
    agy_hooks_debug() {
      local reason="$1"
    
      [ "${AGY_HOOKS_DEBUG:-}" = "1" ] || return 0
    
      local ts
      ts="$(date '+%Y-%m-%dT%H:%M:%S%z')"
      local line="[${ts}] agy-hooks: ${reason}"
    
      if [ -n "${AGY_HOOKS_DEBUG_FILE:-}" ]; then
        printf '%s\n' "$line" >> "$AGY_HOOKS_DEBUG_FILE"
      else
        printf '%s\n' "$line" >&2
      fi
    }
    
  • hooks/agy-subagent-policy.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    #
    # hooks/agy-subagent-policy.sh
    #
    # SubagentStart hook. Reads a hook-event JSON payload on stdin describing the
    # subagent about to be spawned. When -- and ONLY when -- the hook is enabled,
    # the subagent's agent_type is allowlisted, AND `agy-bridge` is available on
    # PATH, this hook emits a fixed advisory as SubagentStart additionalContext:
    #
    #   {"hookSpecificOutput":{"hookEventName":"SubagentStart","additionalContext":"<advisory>"}}
    #
    # In every other case it is completely SILENT (no stdout) and exits 0. This
    # hook must never fail, never hang a subagent spawn, and never echo the
    # incoming prompt/payload back to stdout.
    
    set -uo pipefail
    
    _hook_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    # shellcheck source=./agy-hooks-lib.sh
    source "${_hook_dir}/agy-hooks-lib.sh"
    
    # Read the entire hook-event payload from stdin. Safe even if stdin is empty
    # or already closed -- never blocks. This must run before any guard so stdin
    # is always drained regardless of which branch exits.
    input="$(cat)"
    
    # Guard 1 (FIRST guard that can exit): the hook must be explicitly enabled.
    # This is pure bash (agy_hooks_enabled has no python3 dependency), so the
    # default-OFF path never forks an interpreter.
    if ! agy_hooks_enabled; then
      agy_hooks_debug "disabled -> skip"
      exit 0
    fi
    
    # Guard 2: python3 must actually be usable (not just present on PATH). A
    # present-but-broken python3 (e.g. invocation/import failure) is treated the
    # same as an absent one -- both are "no python3", never misreported as
    # malformed json by the later probe. Only reached when the hook is enabled,
    # so this never forks an interpreter on the default-OFF path. The probe body
    # must not read stdin.
    if ! python3 -c 'import json' >/dev/null 2>&1; then
      agy_hooks_debug "no python3 -> skip"
      exit 0
    fi
    
    # Guard 3: empty or whitespace-only stdin is its own fail-safe branch. A
    # missing payload is operationally distinct from a valid-but-not-allowlisted
    # agent, so it gets its own debug reason (see AGY_HOOKS_DEBUG allowlist
    # discovery UX).
    if [[ -z "${input//[[:space:]]/}" ]]; then
      agy_hooks_debug "empty stdin -> skip"
      exit 0
    fi
    
    # Guard 4: malformed / non-JSON payload is its own fail-safe branch. One
    # python3 validity probe (never eval); a broken payload must be
    # distinguishable from a not-allowlisted agent in the debug log.
    if ! printf '%s' "$input" | python3 -c 'import json,sys; json.loads(sys.stdin.read())' >/dev/null 2>&1; then
      agy_hooks_debug "malformed json -> skip"
      exit 0
    fi
    
    # Parse (never eval) the subagent's agent_type from the payload. Valid JSON
    # with an empty/missing agent_type yields an empty string, which legitimately
    # falls through to the not-allowlisted branch below.
    agent_type="$(agy_hooks_parse_field "$input" agent_type)"
    
    # Guard 5: the subagent's agent_type must be allowlisted.
    if ! agy_hooks_agent_allowed "$agent_type"; then
      agy_hooks_debug "not allowlisted: '${agent_type}' -> skip"
      exit 0
    fi
    
    # Guard 6: agy-bridge must actually be available on PATH.
    if ! command -v agy-bridge >/dev/null 2>&1; then
      agy_hooks_debug "agy-bridge not on PATH -> skip"
      exit 0
    fi
    
    # All guards passed: emit the fixed advisory as SubagentStart
    # additionalContext. The advisory is a fixed python constant -- it is NEVER
    # built via shell string interpolation, and the incoming prompt/payload is
    # never echoed.
    python3 <<'PYEOF'
    import json
    
    advisory = """For general web search, prefer your `WebSearch` tool. For grounded/source-cited search, extended-context reading, or a second opinion you can delegate to agy via the `agy-bridge` command — invoke it through `ctx_shell` (fall back to `Bash` only if `ctx_shell` is unavailable); run `agy-bridge --help`. The judgment stays with you: skip it for small or judgment-heavy tasks, and always verify agy's output."""
    
    output = {
        "hookSpecificOutput": {
            "hookEventName": "SubagentStart",
            "additionalContext": advisory,
        }
    }
    print(json.dumps(output))
    PYEOF
    
    agy_hooks_debug "fired"
    
    exit 0
    

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 withagy-delegate

Claude Code plugin that bridges to agy (Google Antigravity CLI) — adds Gemini, GPT-OSS, and grounded web search to Claude sessions

Get the whole plugin
Stats
17
Stars
2
Forks
Maintained
Maintenance
Shell
Language
1mo ago
Last commit
3mo ago
Created

Repo: davdittrich/delegate-agy