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.
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
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

