Skip to content
Development
Hook

Hooks

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

From plugin
hydraia
852 skills27 agents17 commands6 hooks

Claude Code

> /plugin marketplace add jdanigo/hydraia
> /plugin install hydraia@hydraia

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesstartup|clear|compact"${CLAUDE_PLUGIN_ROOT}/hooks/preflight.sh"

PreToolUse

  • MatchesEdit|Write|MultiEdit"${CLAUDE_PLUGIN_ROOT}/hooks/gate.sh""${CLAUDE_PLUGIN_ROOT}/hooks/blastgate.sh""${CLAUDE_PLUGIN_ROOT}/hooks/gateguard.sh"
  • MatchesTask|Agent"${CLAUDE_PLUGIN_ROOT}/hooks/agents.sh"
  • MatchesBash"${CLAUDE_PLUGIN_ROOT}/hooks/plancheck.sh""${CLAUDE_PLUGIN_ROOT}/hooks/safety-guard.sh""${CLAUDE_PLUGIN_ROOT}/hooks/gateguard.sh""${CLAUDE_PLUGIN_ROOT}/hooks/verifyloop.sh"

PostToolUse

  • MatchesBash"${CLAUDE_PLUGIN_ROOT}/hooks/verifyloop.sh"

PostToolUseFailure

  • MatchesBash"${CLAUDE_PLUGIN_ROOT}/hooks/verifyloop.sh"

SubagentStop

  • "${CLAUDE_PLUGIN_ROOT}/hooks/agents.sh"

Stop

  • "${CLAUDE_PLUGIN_ROOT}/hooks/summary.sh""${CLAUDE_PLUGIN_ROOT}/hooks/delivery-gate.sh"
Read hooks/hooks.json

Where it lives

  • hooks/agents.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Hydraia agent budget (PreToolUse on Task + SubagentStop).
    #
    # Bounds the blast radius of Phase 4: a plan with 135 tasks must NOT fan out into
    # 135 concurrent sub-agents, each loading its own context — that is how a single
    # run burns millions of tokens and blows a usage window. This hook makes the cap a
    # runtime guarantee, not a prompt the model can rationalize past.
    #
    # Two limits, enforced only while a pipeline run is active (docs/hydraia/.active-plan
    # fresh), only in repos that opt in (a docs/hydraia/ directory):
    #
    #   - TOTAL per run (HYDRAIA_MAX_AGENTS, default 30) — the HARD guarantee. Every
    #     dispatch is counted under a lock, so even a same-turn burst of 135 Task calls
    #     is serialized and cut off at the ceiling. Reliable on its own.
    #   - CONCURRENT in flight (HYDRAIA_MAX_CONCURRENT, default 6) — best-effort throttle.
    #     in_flight = dispatched - finished, where "finished" is counted from
    #     SubagentStop. If that completion signal never arrives, the concurrency check
    #     self-disables (never blocks on it) so the pipeline can never deadlock — the
    #     TOTAL cap still bounds the run.
    #
    # Human override (never the model's call): raise a ceiling with an env var, e.g.
    #     export HYDRAIA_MAX_AGENTS=50
    #     export HYDRAIA_MAX_CONCURRENT=10
    # HYDRAIA_ALLOW_DIRECT=1 lifts both caps entirely.
    #
    # On any internal error this hook ALLOWS (fail-open) — it must never wedge a run.
    set -uo pipefail
    
    # shellcheck source=/dev/null
    . "$(dirname "$0")/config.sh" 2>/dev/null || true
    if command -v hy_config >/dev/null 2>&1; then
      MAX_TOTAL="$(hy_config maxTotalAgents 30 HYDRAIA_MAX_AGENTS)"
      MAX_CONCURRENT="$(hy_config maxConcurrentAgents 6 HYDRAIA_MAX_CONCURRENT)"
    else
      MAX_TOTAL="${HYDRAIA_MAX_AGENTS:-30}"
      MAX_CONCURRENT="${HYDRAIA_MAX_CONCURRENT:-6}"
    fi
    # Guard against a non-numeric config value.
    case "$MAX_TOTAL" in ''|*[!0-9]*) MAX_TOTAL=30 ;; esac
    case "$MAX_CONCURRENT" in ''|*[!0-9]*) MAX_CONCURRENT=6 ;; esac
    FRESH_SECS=43200   # 12h — a stale .active-plan does not gate anything
    
    payload="$(cat 2>/dev/null || true)"
    command -v python3 >/dev/null 2>&1 || exit 0
    
    # Parse event name, cwd, and tool name from the hook payload.
    parsed="$(printf '%s' "$payload" | python3 -c '
    import sys, json
    try:
        d = json.load(sys.stdin)
        print("\t".join([
            d.get("hook_event_name") or "",
            d.get("cwd") or "",
            d.get("tool_name") or "",
        ]))
    except Exception:
        print("\t\t")
    ' 2>/dev/null || true)"
    event="$(printf '%s' "$parsed" | awk -F'\t' '{print $1}')"
    cwd="$(printf '%s' "$parsed" | awk -F'\t' '{print $2}')"
    tool="$(printf '%s' "$parsed" | awk -F'\t' '{print $3}')"
    
    # Resolve the repo and confirm Hydraia opt-in.
    base="${cwd:-$PWD}"; [ -d "$base" ] || base="$PWD"
    repo="$(git -C "$base" rev-parse --show-toplevel 2>/dev/null || true)"
    [ -n "$repo" ] || exit 0
    
    # Resolve the artifacts base (in-repo docs/hydraia, or the external dir chosen at the
    # storage gate). Opt-in: base exists, OR repo registered in global config, OR legacy
    # in-repo docs/hydraia/.
    hbase="$(cd "$repo" 2>/dev/null && hy_artifacts_dir)"
    [ -n "$hbase" ] || hbase="$repo/docs/hydraia"
    if [ ! -d "$hbase" ] \
       && [ -z "$(cd "$repo" 2>/dev/null && hy_repo_config artifactsDir "")" ] \
       && [ ! -d "$repo/docs/hydraia" ]; then
      exit 0
    fi
    
    plan="$hbase/.active-plan"
    adir="$hbase/.agents"
    now="$(date +%s)"
    mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
    
    # The sub-agent tool is "Agent" on current Claude Code ("Task" on older builds). Matching
    # only "Task" silently disabled every cap below — accept both.
    is_dispatch=0
    case "$tool" in Task|Agent) is_dispatch=1 ;; esac
    
    # Extract subagent_type (namespace-stripped: "hydraia:hydraia-reviewer" → "hydraia-reviewer"),
    # the explicit model, and the description from the dispatch payload.
    brk="$(printf '%s' "$payload" | python3 -c '
    import sys, json
    try:
        d = json.load(sys.stdin); ti = d.get("tool_input") or {}
        clean = lambda v: " ".join(str(v or "").split())   # no tabs/newlines: one record, 3 fields
        print("\t".join([clean(ti.get("subagent_type")), clean(ti.get("model")),
                         clean(ti.get("description"))]))
    except Exception:
        print("\t\t")
    ' 2>/dev/null || true)"
    sub_type="$(printf '%s' "$brk" | awk -F'\t' '{print $1}')"
    sub_model="$(printf '%s' "$brk" | awk -F'\t' '{print $2}')"
    sub_desc="$(printf '%s' "$brk" | awk -F'\t' '{print $3}')"
    sub_base="${sub_type##*:}"
    
    # --- Opus gate: Opus only judges (independent of an active run) ----------------------
    # Policy: the orchestrator session + the judge reviewers run on Opus; everything else runs
    # on Sonnet (or Haiku). A dispatch is "effective Opus" when it names an opus model, or when
    # it is an unpinned generic agent with no model (it silently inherits the parent's Opus).
    # Executors may run on Opus only when the human chose the Max-quality routing at Phase 3
    # (recorded in <base>/.agents/routing). Human bypass: HYDRAIA_ALLOW_OPUS=1.
    if [ "$event" = "PreToolUse" ] && [ "$is_dispatch" = 1 ] \
       && [ -z "${HYDRAIA_ALLOW_DIRECT:-}" ] && [ -z "${HYDRAIA_ALLOW_OPUS:-}" ]; then
      OPUS_GATE="strict"; command -v hy_config >/dev/null 2>&1 && OPUS_GATE="$(cd "$repo" && hy_config opusGate strict HYDRAIA_OPUS_GATE)"
      if [ "$OPUS_GATE" != "off" ]; then
        eff_opus=0
        case "$sub_model" in *opus*|*Opus*) eff_opus=1 ;; esac
        if [ -z "$sub_model" ]; then
          case "$sub_base" in ''|general-purpose|Explore|Plan|claude) eff_opus=1 ;; esac
        fi
        if [ "$eff_opus" = 1 ]; then
          allowed=0
          case "$sub_base" in hydraia-reviewer|security-reviewer) allowed=1 ;; esac
          if [ "$allowed" = 0 ]; then
            case "$sub_base" in
              hydraia-executor|qa-automation)
                rf="$adir/routing"
                if [ -f "$rf" ] && [ $(( now - $(mtime "$rf") )) -lt "$FRESH_SECS" ] \
                   && [ "$(head -1 "$rf" 2>/dev/null | tr -d '[:space:]')" = "max-quality" ]; then
                  allowed=1
                fi ;;
            esac
    
  • hooks/baseline.shGitHub
  • hooks/blastgate.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Hydraia blast-radius gate (PreToolUse on Edit|Write|MultiEdit).
    #
    # Blocks edits to sensitive paths (secrets, auth, payments, migrations, …) defined in
    # gate.yaml's denylist, and warns past a per-run file-count ceiling. This is the safety
    # gate the spec-drive gate.sh does NOT provide. Distinct concern, separate file.
    #
    # Blocks (exit 2) only when ALL hold: target repo opts in, human bypass unset, pathGate
    # not off, target is not a pipeline artifact/markdown, and the path matches a denylist glob.
    # On any internal error it ALLOWS (fail-open).
    set -uo pipefail
    # shellcheck source=/dev/null
    . "$(dirname "$0")/config.sh" 2>/dev/null || true
    
    payload="$(cat 2>/dev/null || true)"
    command -v python3 >/dev/null 2>&1 || exit 0
    
    file_path="$(printf '%s' "$payload" | python3 -c '
    import sys, json
    try:
        d = json.load(sys.stdin); ti = d.get("tool_input") or {}
        print(ti.get("file_path") or ti.get("path") or "")
    except Exception:
        print("")
    ' 2>/dev/null || true)"
    [ -n "$file_path" ] || exit 0
    
    # Human bypass.
    [ -n "${HYDRAIA_ALLOW_DIRECT:-}" ] && exit 0
    
    # Resolve repo + opt-in (identical test to gate.sh).
    # Nearest EXISTING ancestor (a Write may create new dirs) — never fall back to "." (the
    # hook's cwd), which would misattribute an out-of-repo path to this repo.
    dir="$(dirname "$file_path" 2>/dev/null || echo /)"
    while [ ! -d "$dir" ] && [ "$dir" != "/" ] && [ "$dir" != "." ]; do dir="$(dirname "$dir")"; done
    [ -d "$dir" ] || exit 0
    repo="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || true)"
    [ -n "$repo" ] || exit 0
    adir="$(cd "$repo" 2>/dev/null && hy_artifacts_dir)"; [ -n "$adir" ] || adir="$repo/docs/hydraia"
    if [ ! -d "$adir" ] \
       && [ -z "$(cd "$repo" 2>/dev/null && hy_repo_config artifactsDir "")" ] \
       && [ ! -d "$repo/docs/hydraia" ]; then
      exit 0
    fi
    
    # Mode.
    PATH_GATE="strict"
    command -v hy_config >/dev/null 2>&1 && PATH_GATE="$(hy_config pathGate strict HYDRAIA_PATH_GATE)"
    [ "$PATH_GATE" = "off" ] && exit 0
    
    # Breaker state is the hooks' own ledger — the model never edits it (clearing a STALLED
    # run or a spent budget is the human's call). routing is excluded: Phase 3 records the
    # human's routing choice there.
    case "$file_path" in
      "$adir"/.agents/ledger.json|"$adir"/.agents/verify.json|"$adir"/.agents/dispatched|"$adir"/.agents/finished|"$adir"/.agents/runid)
        echo "[hydraia] BLOCKED: Hydraia breaker state is written only by its hooks. Resetting it is the human's decision — tell the human instead." >&2
        exit 2 ;;
    esac
    
    # Exempt pipeline artifacts + markdown (same as gate.sh).
    case "$file_path" in *.md|*.markdown) exit 0 ;; esac
    case "$file_path" in "$adir"/*|"$repo"/docs/hydraia/*|docs/hydraia/*) exit 0 ;; esac
    
    # Repo-relative path for glob matching.
    rel="${file_path#"$repo"/}"
    
    # Load denylist from gate.yaml (repo), else built-in default. Match with python fnmatch
    # (glob '**' handled by also testing each path suffix). Prints "HIT <rule>" or nothing.
    gy="$repo/gate.yaml"
    hit="$(HY_REL="$rel" HY_GY="$gy" python3 -c '
    import os, fnmatch
    rel = os.environ["HY_REL"]; gy = os.environ["HY_GY"]
    default = [".env",".env.*","**/secrets/**","**/credentials/**","**/*_key*","**/*_secret*",
               ".terraform/**","k8s/production/**","**/migrations/**","auth/**","payments/**","billing/**"]
    rules = []
    try:
        inlist = False
        for line in open(gy):
            s = line.strip()
            if s.startswith("denylist:"): inlist = True; continue
            if inlist:
                if s.startswith("- "):
                    rules.append(s[2:].strip().strip("\"'"'"'"))
                elif s and not s.startswith("#") and not s.startswith("- "):
                    break
    except Exception:
        rules = []
    if not rules: rules = default
    def match(rule, path):
        if fnmatch.fnmatch(path, rule): return True
        # emulate "**/" prefix and "/**" suffix against path segments
        r = rule.replace("**/", "").replace("/**", "")
        if fnmatch.fnmatch(path, r) or fnmatch.fnmatch(path, "*/"+r) or fnmatch.fnmatch(path, r+"/*"): return True
        if ("/"+rule.replace("**","").strip("/")+"/") in ("/"+path+"/"): return True
        return False
    for r in rules:
        if match(r, rel): print("HIT "+r); break
    ' 2>/dev/null || true)"
    
    if [ -n "$hit" ]; then
      rule="${hit#HIT }"
      cat >&2 <<EOF
    [hydraia] BLOCKED: blast-radius gate.
    
    The path "$rel" matches a denylisted rule in gate.yaml: "$rule".
    Hydraia refuses edits to secrets, auth, payments, infra, and migration paths — even
    under model instruction — because a wrong edit here is high-blast-radius.
    
    If this edit is genuinely intended, the HUMAN authorizes it (never the model):
      export HYDRAIA_ALLOW_DIRECT=1
    or remove/adjust the rule in gate.yaml. To disable the gate entirely: set pathGate=off.
    EOF
      exit 2
    fi
    
    # --- Plan-scope gate (per active-plan run) ----------------------------------
    # A frozen plan declares, per task, the files it may touch (`**Files:**` lines). An edit
    # outside that union is scope creep — the model widening the change without asking. Block
    # it and send the decision back to the human (the executor reports NEEDS_DECISION; the
    # human amends the plan and re-arms). Legacy plans with no **Files:** lines → no gate.
    # Always allowed: lockfiles (regenerated by package managers). Markdown + artifacts are
    # exempted above. scopeGate: strict (default) | warn | off.
    aplan="$adir/.active-plan"
    if [ -f "$aplan" ]; then
      apm="$(stat -c %Y "$aplan" 2>/dev/null || stat -f %m "$aplan" 2>/dev/null || echo 0)"
      if [ $(( $(date +%s) - apm )) -lt 43200 ]; then
        SCOPE="strict"; command -v hy_config >/dev/null 2>&1 && SCOPE="$(cd "$repo" && hy_config scopeGate strict HYDRAIA_SCOPE_GATE)"
        if [ "$SCOPE" != "off" ]; then
          ppath="$(head -1 "$aplan" 2>/dev/null | tr -d '\r' | sed 's/[[:space:]]*$//')"
          case "$ppath" in /*) : ;; ?*) ppath="$repo/$ppath" ;; esac
          if [ -n "$ppath" ] && [ -f "$ppath" ]; then
            verdict="$(HY_PLAN="$ppath" HY_REL="$rel" HY_REPO="$repo" python3 -c '
    import os, re, fnmatch
    rel = os.environ["HY_RE
  • hooks/config.shGitHub
  • hooks/delivery-gate.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Hydraia delivery-gate (Stop hook). ECC-inspired. Deterministic — no AI, zero tokens.
    #
    # Scans the session transcript tail for rationalization patterns ("skip tests for now",
    # "pre-existing bug", "good enough", "will fix later", "disable the lint/type rule") and
    # WARNS (never blocks — avoids false positives). Also flags an incomplete sprint-status
    # (an Agile run that stopped mid-epic). Warn-only: always exits 0.
    set -uo pipefail
    # shellcheck source=/dev/null
    . "$(dirname "$0")/config.sh" 2>/dev/null || true
    
    payload="$(cat 2>/dev/null || true)"
    MODE="on"; command -v hy_config >/dev/null 2>&1 && MODE="$(hy_config deliveryGate on HYDRAIA_DELIVERY_GATE)"
    [ "$MODE" = "off" ] && exit 0
    command -v python3 >/dev/null 2>&1 || exit 0
    
    tpath="$(printf '%s' "$payload" | python3 -c '
    import sys, json
    try:
        print((json.load(sys.stdin) or {}).get("transcript_path") or "")
    except Exception:
        print("")
    ' 2>/dev/null || true)"
    
    if [ -n "$tpath" ] && [ -f "$tpath" ]; then
      hits="$(tail -c 200000 "$tpath" 2>/dev/null | tr 'A-Z' 'a-z' | grep -oE \
        'skip(ping)? (the )?tests? for now|pre-existing bug|good enough for now|will fix (this )?later|disable the (lint|type|eslint|ruff) rule|ignore the (test|type) (error|failure)' \
        2>/dev/null | sort -u | head -5 || true)"
      if [ -n "$hits" ]; then
        {
          echo "[hydraia] delivery-gate — rationalization patterns detected in this session:"
          printf '  • %s\n' $hits 2>/dev/null || echo "  • (see transcript)"
          echo "These often precede shipped defects. Confirm tests actually run and nothing was waved through."
        } >&2
      fi
    fi
    
    # Incomplete Agile epic warning (best-effort).
    repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"
    if [ -n "$repo" ]; then
      adir="$(cd "$repo" 2>/dev/null && hy_artifacts_dir 2>/dev/null)"; [ -n "$adir" ] || adir="$repo/docs/hydraia"
      for ss in "$adir"/epics/*/sprint-status.yaml; do
        [ -f "$ss" ] || continue
        if grep -qE 'phase: (spec|plan|build|review|verify)\b' "$ss" 2>/dev/null; then
          echo "[hydraia] delivery-gate — an Agile epic looks mid-flight ($ss). Resume with /hydraia:resume." >&2
        fi
      done
    fi
    exit 0
    
  • hooks/doctor.shGitHub
  • hooks/gate.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Hydraia spec-drive gate (PreToolUse on Edit/Write/MultiEdit).
    #
    # Enforces the pipeline's core promise: no source-code edit before a spec + plan
    # exist. It makes "never skip a phase" a runtime guarantee instead of a prompt the
    # model can rationalize past.
    #
    # It BLOCKS a code edit only when ALL of these hold:
    #   - the target repo opts in (registered in the global config, or has a resolved
    #     artifacts dir / legacy docs/hydraia/ directory), AND
    #   - the human bypass is NOT set (HYDRAIA_ALLOW_DIRECT is empty/unset), AND
    #   - the target is source code (not markdown, not under the artifacts dir), AND
    #   - no frozen plan is active (<artifacts dir>/.active-plan missing or stale).
    #
    # The bypass is deliberately an env var, not a command: the decision to skip the
    # pipeline is the human's, not the model's. To allow a direct edit:
    #     export HYDRAIA_ALLOW_DIRECT=1
    #
    # On any internal error the gate ALLOWS (fail-open) — it must never wedge editing.
    set -uo pipefail
    
    # shellcheck source=/dev/null
    . "$(dirname "$0")/config.sh" 2>/dev/null || true
    
    FRESH_SECS=43200   # 12h — a stale .active-plan from an old run won't authorize edits
    QUICK_SECS=1800    # 30m — a human-approved quick edit is a short, single-burst window
    
    # Read the hook payload from stdin (Claude Code passes tool input as JSON).
    payload="$(cat 2>/dev/null || true)"
    
    # Extract the target file path. Prefer python3 (a Hydraia prerequisite); if it is
    # missing or the payload is unparseable, fail open.
    file_path=""
    if command -v python3 >/dev/null 2>&1; then
      file_path="$(printf '%s' "$payload" | python3 -c '
    import sys, json
    try:
        d = json.load(sys.stdin)
        ti = d.get("tool_input") or {}
        print(ti.get("file_path") or ti.get("path") or "")
    except Exception:
        print("")
    ' 2>/dev/null || true)"
    fi
    
    # No path to reason about → allow.
    [ -n "$file_path" ] || exit 0
    
    # Human bypass — the only sanctioned way to skip the pipeline.
    if [ -n "${HYDRAIA_ALLOW_DIRECT:-}" ]; then
      exit 0
    fi
    
    # Resolve the repo root for the file being edited.
    dir="$(dirname "$file_path" 2>/dev/null || echo .)"
    [ -d "$dir" ] || dir="."
    repo="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || true)"
    [ -n "$repo" ] || exit 0   # not in a git repo → not a Hydraia-managed edit, allow
    
    # Resolve the artifacts base (in-repo docs/hydraia by default, or the external dir
    # the user chose at the storage gate). Run from the repo so git-root resolution is
    # correct even when the hook's cwd differs.
    adir="$(cd "$repo" 2>/dev/null && hy_artifacts_dir)"
    [ -n "$adir" ] || adir="$repo/docs/hydraia"
    
    # Opt-in: enforce only in repos that use Hydraia — the resolved artifacts dir exists,
    # OR the repo is registered in the global config (external mode leaves nothing in the
    # repo), OR the legacy in-repo docs/hydraia/ exists.
    if [ ! -d "$adir" ] \
       && [ -z "$(cd "$repo" 2>/dev/null && hy_repo_config artifactsDir "")" ] \
       && [ ! -d "$repo/docs/hydraia" ]; then
      exit 0
    fi
    
    # Spec-drive mode (config): off = never gate; relaxed = warn but allow;
    # strict (default) = block. Env HYDRAIA_SPEC_DRIVE overrides the file.
    SPEC_DRIVE="strict"
    command -v hy_config >/dev/null 2>&1 && SPEC_DRIVE="$(hy_config specDrive strict HYDRAIA_SPEC_DRIVE)"
    [ "$SPEC_DRIVE" = "off" ] && exit 0
    
    # Exempt non-code artifacts: markdown (specs, plans, run logs, docs) and anything
    # under docs/hydraia/ (the pipeline's own writes must never be blocked).
    case "$file_path" in
      *.md|*.markdown) exit 0 ;;
    esac
    case "$file_path" in
      "$adir"/*|"$repo"/docs/hydraia/*|docs/hydraia/*) exit 0 ;;
    esac
    
    now="$(date +%s)"
    fresh() { # $1=marker path, $2=max age secs → 0 if present and fresh
      [ -f "$1" ] || return 1
      local m
      m="$(stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0)"
      [ $(( now - m )) -lt "$2" ]
    }
    
    # Allow when a frozen plan is active (normal pipeline, Phase 3+).
    fresh "$adir/.active-plan" "$FRESH_SECS" && exit 0
    
    # Allow when the human approved a one-off quick edit in-conversation. This marker
    # is written by the model ONLY after an explicit AskUserQuestion approval (see
    # SKILL.md "Quick-mode"). It is short-lived and meant to be removed right after the
    # edit — it is a convenience channel, not the hard bypass (that is the env var).
    fresh "$adir/.quick-approved" "$QUICK_SECS" && exit 0
    
    # Relaxed mode: note the missing plan but allow the edit (no block).
    if [ "$SPEC_DRIVE" = "relaxed" ]; then
      echo "[hydraia] note: editing source before a frozen plan (spec-drive=relaxed). Consider /hydraia:feature." >&2
      exit 0
    fi
    
    # Otherwise (strict) block. Exit 2 tells Claude Code to reject the tool call and feed
    # stderr back to the model, which can then recover via the pipeline or quick-mode.
    cat >&2 <<EOF
    [hydraia] BLOCKED: spec-drive gate.
    
    You are editing source code before a frozen plan exists for this work.
    Hydraia is spec-drive-design first — Phases 2 (spec + threat model) and 3
    (plan + self-review) must complete before any code is written.
    
    Recover with ONE of:
      • Run the pipeline: /hydraia:feature <what you are building>
        (or /hydraia:plan first to freeze the design, then execute).
      • Quick-mode (only if this change is genuinely trivial — no new logic, no new
        file, and it does NOT touch auth / PII / external input): ask the human via
        AskUserQuestion whether to skip the design ceremony (pro: far fewer tokens;
        con: no spec-drive record, no double review). If they approve, write the
        approval marker and retry, then run the real build/tests and remove it:
            printf 'reason\n' > "$adir/.quick-approved"
        Never write this marker without an explicit human "yes".
      • Human hard bypass (set by YOU in the shell, un-forgeable by the model):
            export HYDRAIA_ALLOW_DIRECT=1
    
    Token cost or change size is NOT a reason for the model to skip — proportionality
    is the human's call, made via one of the channels above, never the model's own.
    EOF
    exit 2
    
  • hooks/gateguard.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Hydraia gateguard (PreToolUse on Edit|Write|Bash). ECC-inspired. OPT-IN (default off).
    #
    # Forces the agent to record concrete facts before its FIRST write of a run — the files
    # that import the target, the schema/fields touched, and the verbatim task instruction —
    # so autonomous execution investigates instead of guessing. When gateGuard=on and no facts
    # file exists for the run, the first write is blocked (exit 2) with instructions; once the
    # facts file is written, writes proceed. Loop-safe: only the first write is gated per run.
    set -uo pipefail
    # shellcheck source=/dev/null
    . "$(dirname "$0")/config.sh" 2>/dev/null || true
    
    command -v python3 >/dev/null 2>&1 || exit 0
    cat >/dev/null 2>&1 || true   # drain stdin; decision does not need the payload body
    [ -n "${HYDRAIA_ALLOW_DIRECT:-}" ] && exit 0
    
    repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"
    [ -n "$repo" ] || exit 0
    adir="$(cd "$repo" 2>/dev/null && hy_artifacts_dir 2>/dev/null)"; [ -n "$adir" ] || adir="$repo/docs/hydraia"
    
    MODE="off"; command -v hy_config >/dev/null 2>&1 && MODE="$(hy_config gateGuard off HYDRAIA_GATE_GUARD)"
    [ "$MODE" = "on" ] || exit 0
    
    plan="$adir/.active-plan"
    [ -f "$plan" ] || exit 0                 # only meaningful inside an armed run
    adir_agents="$adir/.agents"; mkdir -p "$adir_agents" 2>/dev/null || exit 0
    runid="$(stat -c %Y "$plan" 2>/dev/null || stat -f %m "$plan" 2>/dev/null || echo 0)"
    facts="$adir_agents/facts-$runid"
    
    [ -f "$facts" ] && exit 0                 # facts recorded → allow
    
    cat >&2 <<EOF
    [hydraia] BLOCKED: gateguard — record facts before the first write of this run.
    
    Investigate, don't guess. Write "$facts" containing, for the change you are about to make:
      • the files that import / call the symbol you are editing (grep the codebase),
      • the schema / field names / data shapes you will touch,
      • the verbatim task or instruction you are implementing.
    
    Then retry the write. This gate fires once per run. Disable with gateGuard=off.
    EOF
    exit 2
    
  • hooks/plancheck.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Hydraia plan self-containment gate (PreToolUse on Bash).
    #
    # Fires when the model arms the spec-drive gate — i.e. writes a plan path into
    # docs/hydraia/.active-plan. Before it lets the arm through, it scans that frozen
    # plan's TASK bodies for "reference smells": phrasings that point the executor at
    # the spec or another document for content it must produce ("follow spec §3",
    # "see the design", "as in the spec"). Those break the pipeline's core promise —
    # that ANY cheap, context-less executor (Haiku, Sonnet 5, Gemini Flash, Codex)
    # can run a task from its block alone. A task that references external content is
    # NOT self-contained: the cheap model can't see the spec, so it guesses, truncates,
    # or gets creative. This gate turns "inline everything" from a prompt the planner
    # might forget into a runtime block it cannot.
    #
    # It BLOCKS the arm only when ALL hold:
    #   - the command writes to docs/hydraia/.active-plan, AND
    #   - the human bypass is NOT set (HYDRAIA_ALLOW_DIRECT empty/unset), AND
    #   - the referenced plan file exists and its task bodies contain a reference smell,
    #     a UI task without visual direction, or a task missing its contract
    #     (**Files:** + a Verify line).
    #
    # On any internal error it ALLOWS (fail-open) — it must never wedge the pipeline.
    set -uo pipefail
    
    [ -n "${HYDRAIA_ALLOW_DIRECT:-}" ] && exit 0
    
    payload="$(cat 2>/dev/null || true)"
    [ -n "$payload" ] || exit 0
    
    cmd=""
    if command -v python3 >/dev/null 2>&1; then
      cmd="$(printf '%s' "$payload" | python3 -c '
    import sys, json
    try:
        d = json.load(sys.stdin)
        ti = d.get("tool_input") or {}
        print(ti.get("command") or "")
    except Exception:
        print("")
    ' 2>/dev/null || true)"
    fi
    [ -n "$cmd" ] || exit 0
    
    # Resolve the artifacts base for this repo (in-repo docs/hydraia, or the external dir
    # chosen at the storage gate) so the arm-target and plan-path match in both modes.
    # shellcheck source=/dev/null
    . "$(dirname "$0")/config.sh" 2>/dev/null || true
    abase="docs/hydraia"
    command -v hy_artifacts_dir >/dev/null 2>&1 && abase="$(hy_artifacts_dir)"
    
    case "$cmd" in
      *docs/hydraia/.active-plan*|*"$abase"/.active-plan*) : ;;
      *) exit 0 ;;
    esac
    
    # Match the plan path in either mode: a token ending in /plans/<name>.md, whether
    # in-repo (docs/hydraia/plans/…) or an absolute external path (…/plans/…).
    plan="$(printf '%s' "$cmd" | grep -oE '[^"'"'"' ]*/plans/[^"'"'"' ]+\.md' | head -1 || true)"
    [ -n "$plan" ] || exit 0
    [ -f "$plan" ] || exit 0
    
    # Task headings are "## Task <n>" or "### Task <n>" (plans use both; "## Tasks" / "## Task list"
    # section titles are not tasks). Matching only "###" silently skipped every level-2 plan.
    # Fenced code blocks are skipped (a heading inside an example is not a task).
    smells="$(awk '/^```/{f=!f; next} f{next} /^###? +Task +[0-9A-Z]/{t=1} t' "$plan" 2>/dev/null | grep -nEi \
      'follow (it|the skeleton|the above)|(see|per|refer to|copy from|copy the|as in|as described in|as defined in|as shown in) (the )?(spec|design)|the skeleton (in|from) the spec|§[0-9]|see (the )?design doc' \
      2>/dev/null || true)"
    
    # UI tasks must carry their visual direction inline (Phase 3 rule). A task whose body
    # touches UI surfaces — a frontend file extension or an explicit UI keyword — but names
    # none of the visual-direction anchors (ui-ux-pro-max, palette, type scale, spacing,
    # interaction states, …) will fall back to a generic look on a weak executor. Per-task,
    # conservative: flag only strong UI signals with zero visual direction. Fail-open.
    ui_smells="$(awk '
      /^```/ { fence=!fence; next }
      fence { next }
      /^###? +Task +[0-9A-Z]/ { if (inblock) evaluate(); inblock=1; buf=""; title=$0; next }
      inblock { buf = buf "\n" tolower($0) }
      END { if (inblock) evaluate() }
      function evaluate(   isui, hasdir) {
        isui   = (buf ~ /\.(tsx|jsx|vue|svelte|css|scss|sass|less|styl|astro|html)([^a-z]|$)/) \
                 || (buf ~ /markup|stylesheet|css module|tailwind class|jsx element/)
        hasdir = (buf ~ /ui-ux-pro-max|palette|type scale|typograph|spacing scale|interaction state|visual direction|color palette|design token|font pairing|accessibility floor|wcag/)
        if (isui && !hasdir) print title
      }
    ' "$plan" 2>/dev/null || true)"
    
    # Plan-as-contract: every task declares the files it may touch (**Files:** — also what the
    # blastgate plan-scope gate enforces) and how it proves itself done (a Verify line: exact
    # command + expected result — the executor's stop condition). A task without them is how a
    # run drifts and never knows when to stop. planContract=off (HYDRAIA_PLAN_CONTRACT) disables.
    PCONTRACT="strict"
    command -v hy_config >/dev/null 2>&1 && PCONTRACT="$(hy_config planContract strict HYDRAIA_PLAN_CONTRACT)"
    contract_missing=""
    if [ "$PCONTRACT" != "off" ]; then
      contract_missing="$(awk '
        /^```/ { fence=!fence; next }
        fence { next }
        /^###? +Task +[0-9A-Z]/ { if (inblock) evaluate(); inblock=1; files=0; verify=0; title=$0; next }
        inblock && /\*\*Files:?\*\*:?/ { files=1 }
        inblock && /[Vv]erif(y|ies|ication)/ { verify=1 }
        END { if (inblock) evaluate() }
        function evaluate() {
          if (!files || !verify) print title "  (missing:" (files ? "" : " **Files:**") (verify ? "" : " Verify") ")"
        }
      ' "$plan" 2>/dev/null || true)"
    fi
    
    [ -z "$smells" ] && [ -z "$ui_smells" ] && [ -z "$contract_missing" ] && exit 0
    
    {
      echo "[hydraia] BLOCKED: plan is not ready to freeze."
      echo
      if [ -n "$smells" ]; then
        cat <<EOF
    Not self-contained — the plan points the executor at the spec or another document for
    content it must produce. A cheap, context-less executor (Haiku, Sonnet 5, Gemini
    Flash, Codex) CANNOT see the spec — so it will guess, truncate, or get creative.
    
    Offending task lines (relative to the first "### Task"):
    $smells
    
    Fix: INLINE the referenced content into the task itself — the full verbatim file
    body, the exact code/skeleton, the literal old_string->new_string. Duplicate it out
    of the spec even though it repeats: here DRY yields to s
  • hooks/preflight.shRunsGitHub
  • hooks/safety-guard.shRunsGitHub
  • hooks/summary.shRunsGitHub
  • hooks/verifyloop.shRunsGitHub

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

@jdanigo
Ships withhydraia

An agentic development harness for Claude Code. **One command runs the entire feature pipeline** — it collaborates with you on the design, then builds autonomously: plan, execute, double-review, and verify.

852 skills27 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
8
Star history of jdanigo/hydraia on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook318k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks191k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3117k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+312k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in hydraia.

Which coding agents can use the hooks in hydraia?
The hooks in hydraia install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.