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 commands4 hooks
Install
> /plugin marketplace add jdanigo/hydraia
> /plugin install hydraia@hydraia

Ships with hydraia. Installing the plugin gets these hooks.

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"${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"

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; }
    
    # Enforce only during an active run (Phase 3 armed the plan, Phase 6 disarms it).
    [ -f "$plan" ] || exit 0
    pm="$(mtime "$plan")"
    [ $(( now - pm )) -lt "$FRESH_SECS" ] || exit 0
    
    mkdir -p "$adir" 2>/dev/null || exit 0
    
    # Reset per-run counters when a new run is detected (plan mtime changed).
    reset_if_new_run() {
      local rid="$adir/runid" cur
      cur="$(cat "$rid" 2>/dev/null || echo)"
      if [ "$cur" != "$pm" ]; then
        : > "$adir/dispatched" 2>/dev/null || true
        : > "$adir/finished"   2>/dev/null || true
        : > "$adir/ledger.json" 2>/dev/null || true
        printf '%s' "$pm" > "$rid" 2>/dev/null || true
      fi
    }
    
    # --- SubagentStop: record a completion, nothing to block ---------------------
    # Sub-agent TOKEN/MODEL telemetry is NOT captured here: this hook does not fire
    # reliably for every sub-agent (e.g. background dispatches), and Claude Code already
    # persists each sub-agent's full transcript on disk at
    # <project>/<sessionId>/subagents/agent-<id>.jsonl (+ .meta.json with its agentType).
    # summary.sh reads that directory directly at run close — deterministic and
    # hook-independent. Here we only bump the concurrency completion counter.
    if [ "$event" = "SubagentStop" ]; then
      reset_if_new_run
      printf '1\n' >> "$adir/finished" 2>/dev/null || true
      exit 0
    fi
    
    # --- PreToolUse: only the Task tool is capped --------------------------------
    [ "$tool" = "Task" ] || exit 0
    
    # Human bypass lifts the caps.
    [ -n "${HYDRAIA_ALLOW_DIRECT:-}" ] && exit 0
    
    # --- Kill switch + token caps ------------------------------------------------
    # Kill switch: config loopPause or env HYDRAIA_PAUSE blocks ALL dispatch immediately.
    PAUSED="false"; command -v hy_config >/dev/null 2>&1 && PAUSED="$(hy_config loopPause false HYDRAIA_PAUSE)"
    if [ "$PAUSED" = "true" ] || [ -n "${HYDRAIA_PAUSE:-}" ]; then
      cat >&2 <<EOF
    [hydraia] BLOCKED: loop paused (kill switch).
    
    Sub-agent dispatch is disabled (loopPause / HYDRAIA_PAUSE). Switch to report-only.
    Clear the pause to resume: unset HYDRAIA_PAUSE (or set loopPause=false in config).
    EOF
      exit 2
    fi
    
    # Token caps (default 0 = off). Sum today's spend from the telemetry summary.sh writes.
    DAILY_CAP="$(hy_config dailyTokenCap 0 HYDRAIA_DAILY_TOKEN_CAP 2>/dev/null || echo 0)"
    RUN_CAP="$(hy_config perRunTokenCap 0 HYDRAIA_RUN_TOKEN_CAP 2>/dev/null || echo 0)"
    case "$DAILY_CAP" in ''|*[!0-9]*) DAILY_CAP=0 ;; esac
    case "$RUN_CAP" in ''|*[!0-9]*) RUN_CAP=0 ;; esac
    TELEM="${HOME}/.cache/hydraia/telemetry.jsonl"
    if { [ "$DAILY_CAP" -gt 0 ] || [ "$RUN_CAP" -gt 0 ]; } && [ -f "$TELEM" ]; th
  • 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).
    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
    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
    
    # 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
    
    # --- maxFiles advisory (per active-plan run) --------------------------------
    # Count distinct files edited this run. Warn past maxFiles; block only if enforced.
    plan="$adir/.active-plan"
    [ -f "$plan" ] || exit 0                 # only meaningful during an active run
    acount_dir="$adir/.agents"; mkdir -p "$acount_dir" 2>/dev/null || exit 0
    efile="$acount_dir/edited-files"
    # Reset the maxFiles set per run, like the other counters. The run id is the .active-plan
    # mtime; when it changes (a new run armed the plan) the previous run's file set is stale,
    # so truncate it and record the new run id before counting. Fail-open on any error.
    rid_file="$acount_dir/edited-files.runid"
    cur_runid="$(stat -c %Y "$plan" 2>/dev/null || stat -f %m "$plan" 2>/dev/null || echo 0)"
    prev_runid="$(cat "$rid_file" 2>/dev/null || echo)"
    if [ "$cur_runid" != "$prev_runid" ]; then
      : > "$efile" 2>/dev/null || true
      printf '%s' "$cur_runid" > "$rid_file" 2>/dev/null || true
    fi
    grep -qxF "$rel" "$efile" 2>/dev/null || printf '%s\n' "$rel" >> "$efile" 2>/dev/null || true
    MAXF="10"; command -v hy_config >/dev/null 2>&1 && MAXF="$(hy_config maxFiles 10)"
    case "$MAXF" in ''|*[!0-9]*) MAXF=10 ;; esac
    # gate.yaml maxFiles overrides config default if present.
    gyf="$(grep -E '^maxFiles:' "$gy" 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)"
    [ -n "$gyf" ] && MAXF="$gyf"
    n="$(sort -u "$efile" 2>/dev/null | wc -l | tr -d ' ')"; n="${n:-0}"
    if [ "$n" -gt "$MAXF" ]; then
      ENF="false"; command -v hy_config >/dev/null 2>&1 && ENF="$(hy_config maxFilesEnforce false HYDRAIA_MAX_FILES_ENFORCE)"
      if [ "$ENF" = "true" ]; then
        echo "[hydraia] BLOCKED: blast-radius — this run has touched $n distinct files (max $MAXF). Consolidate or raise maxFiles." >&2
        exit 2
      fi
      echo "[hydraia] note: this run has touched $n distinct files (advisory max $MAXF). Large diff — confirm scope." >&2
    fi
    exit 0
    
  • 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.
    #
    # 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
    
    smells="$(awk '/^### +Task/{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 '
      /^### +Task/ { 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)"
    
    [ -z "$smells" ] && [ -z "$ui_smells" ] && 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 self-containment.
    EOF
        echo
      fi
      if [ -n "$ui_smells" ]; then
        cat <<EOF
    UI task without visual direction — these tasks touch a user-visible surface but carry
    none of the Phase 2 UX / visual direction inline (style, palette, type scale, spacing,
    interaction states, ui-ux-pro-max). On a weak autonomous executor that produces flat,
    generic output — the exact failure the frontend rule exists to prevent.
    
    Offending tasks:
    $ui_smells
    
    Fix: embed the concrete visual decisions from the Phase 2 spec's UX / visual direction
    section into each UI task body (exact style, palette values, type scale, spacing,
    interaction states, WCAG floor). The executor implements these inline values directly —
    it has no Skill tool and does not invoke ui-ux-pro-max; that skill runs once at Phase 2
    design time and the inlined direction IS the visual system.
    EOF
        echo
      fi
      echo "Then re-arm the gate. (Human hard bypass, if this is a false positive:"
      echo "export HYDRAIA_ALLOW_DIRECT=1)"
    } >&2
    exit 2
    
  • hooks/preflight.shRunsGitHub
  • hooks/safety-guard.shRunsGitHub
  • hooks/summary.shRunsGitHub

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

Get the whole plugin