Hooks
What hydraia runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /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.
- Matches
startup|clear|compact"${CLAUDE_PLUGIN_ROOT}/hooks/preflight.sh"
PreToolUse
- Matches
Edit|Write|MultiEdit"${CLAUDE_PLUGIN_ROOT}/hooks/gate.sh""${CLAUDE_PLUGIN_ROOT}/hooks/blastgate.sh""${CLAUDE_PLUGIN_ROOT}/hooks/gateguard.sh" - Matches
Task"${CLAUDE_PLUGIN_ROOT}/hooks/agents.sh" - Matches
Bash"${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"
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.
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.
Repo: jdanigo/hydraia

